Files
smartDriveEEUniApp/pages/customer/customer.vue
2025-12-03 08:29:05 +08:00

2639 lines
71 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="page">
<!-- #ifdef APP -->
<statusBar></statusBar>
<!-- #endif -->
<!-- 导航栏 -->
<uni-nav-bar
:fixed="true"
:statusBar="true"
title="AI销冠系统"
leftIcon="left"
@clickLeft="goBack"
color="#333"
backgroundColor="#FFFFFF">
</uni-nav-bar>
<view class="content">
<!-- 标签页 -->
<view class="tabs">
<view
class="tab-item"
:class="{ active: activeTab === 'service' }"
@click="switchTab('service')">
<text>服务记录</text>
</view>
<view
class="tab-item"
:class="{ active: activeTab === 'customer' }"
@click="switchTab('customer')">
<text>客户</text>
</view>
</view>
<!-- 服务记录列表 -->
<ServiceList
v-if="activeTab === 'service'"
:list="serviceList"
:total="serviceListTotal"
@itemClick="viewDetail"
@filterClick="refreshServiceList"
@reachBottom="onServiceListReachBottom" />
<!-- 客户列表 -->
<CustomerList
v-if="activeTab === 'customer'"
:list="customerList"
@itemClick="viewCustomerDetail" />
</view>
<!-- 客户档案弹窗 -->
<CustomerProfileModal
ref="customerProfileModal"
:profile="currentCustomerProfile"
@close="onCustomerProfileClose" />
<!-- 服务记录详情弹窗 -->
<uni-popup ref="serviceDetailPopup" type="bottom" :mask-click="false">
<view class="service-detail-modal">
<!-- 顶部栏 -->
<view class="modal-header">
<view class="modal-close" @click="closeDetail">
<uni-icons type="close" size="24" color="#333"></uni-icons>
</view>
<text class="modal-title">{{ currentServiceDetail.title }}</text>
<view class="modal-share" @click="shareDetail">
<uni-icons type="forward" size="20" color="#333"></uni-icons>
</view>
</view>
<!-- 日期时间 -->
<view class="modal-date">
<text>{{ currentServiceDetail.date }}</text>
</view>
<!-- AI总结横幅 -->
<view class="modal-banner">
<view class="banner-left">
<view class="banner-icon">
<uni-icons type="loop" size="18" color="#007AFF"></uni-icons>
</view>
<text class="banner-text">AI总结提炼关键, 为销售减负</text>
</view>
</view>
<!-- 音频播放器 -->
<view class="audio-player">
<view class="play-btn" @click="togglePlay">
<uni-icons :type="isPlaying ? 'pause-filled' : 'play-filled'" size="24" color="#007AFF"></uni-icons>
</view>
<view class="progress-bar">
<view class="progress-line">
<view class="progress-dot"></view>
</view>
</view>
<text class="audio-time">00:00:00/00:00:00</text>
<view class="speed-btn">
<text>DD 1.0</text>
</view>
</view>
<!-- 标签页 -->
<view class="detail-tabs">
<view
class="detail-tab-item"
:class="{ active: activeDetailTab === 'summary' }"
@click="switchDetailTab('summary')">
<text>会话总结</text>
</view>
<view
class="detail-tab-item"
:class="{ active: activeDetailTab === 'todo' }"
@click="switchDetailTab('todo')">
<text>智能待办</text>
</view>
<view
class="detail-tab-item"
:class="{ active: activeDetailTab === 'chapter' }"
@click="switchDetailTab('chapter')">
<text>章节概要</text>
</view>
<view
class="detail-tab-item"
:class="{ active: activeDetailTab === 'performance' }"
@click="switchDetailTab('performance')">
<text>服务表现</text>
</view>
</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>
<!-- 服务表现内容 -->
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab === 'performance'">
<view class="performance-score">
<text class="score-label">本次服务评分</text>
<text class="score-value">{{ performanceData.totalScore }}</text>
</view>
<view
class="performance-category"
v-for="(category, categoryIndex) in performanceData.categories"
:key="categoryIndex">
<view class="category-header" @click="toggleCategory(categoryIndex)">
<view class="category-left">
<view class="category-bullet" :class="category.completed ? 'bullet-blue' : 'bullet-gray'"></view>
<text class="category-name">{{ category.name }}</text>
</view>
<uni-icons
:type="category.expanded ? 'up' : 'down'"
size="16"
color="#999">
</uni-icons>
</view>
<view class="category-content" v-if="category.expanded">
<!-- AI横幅如果有 -->
<view class="performance-banner" v-if="category.showBanner">
<view class="banner-left">
<view class="banner-icon">
<uni-icons type="loop" size="18" color="#007AFF"></uni-icons>
</view>
<text class="banner-text">AI总结提炼关键,为销售减负</text>
</view>
</view>
<!-- 音频播放器如果有 -->
<view class="performance-audio" v-if="category.showAudio">
<view class="audio-player">
<view class="play-btn" @click="togglePlay">
<uni-icons :type="isPlaying ? 'pause-filled' : 'play-filled'" size="24" color="#007AFF"></uni-icons>
</view>
<view class="progress-bar">
<view class="progress-line">
<view class="progress-dot"></view>
</view>
</view>
<text class="audio-time">00:00:00/00:00:00</text>
<view class="speed-btn">
<text>DD 1.0</text>
</view>
</view>
</view>
<!-- 子项列表 -->
<view
class="performance-item"
v-for="(item, itemIndex) in category.items"
:key="itemIndex"
:class="{ 'item-completed': item.completed }">
<view class="item-content" @click="toggleItem(categoryIndex, itemIndex)">
<view class="item-left">
<text class="item-name">{{ item.name }}</text>
<text class="item-status" :class="item.completed ? 'status-completed' : 'status-missing'" v-if="!item.completed">
{{ item.completed ? '已完成' : '未提及' }}
</text>
<text class="item-points">+{{ item.points }}</text>
</view>
<uni-icons type="down" size="16" color="#999"></uni-icons>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 智能待办内容 -->
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab === 'todo'">
<view class="todo-list" v-if="todoList.length > 0">
<view
class="todo-item"
v-for="(item, index) in todoList"
:key="index"
:class="{
'todo-completed': item.status && (item.status.includes('完成') || item.status === '已完成'),
'todo-clickable': isTodoPending(item.status)
}">
<view class="todo-header">
<view class="todo-title-wrapper">
<view class="todo-status-dot" :class="getTodoStatusClass(item.status)"></view>
<text class="todo-title">{{ item.todoTitle || item.title || '待办事项' }}</text>
</view>
<view class="todo-status-wrapper" v-if="isTodoPending(item.status)">
<text
class="todo-status todo-status-clickable"
:class="getTodoStatusTextClass(item.status)"
:id="'todo-status-' + index"
@click.stop="toggleTodoMenu(index, $event)">
{{ item.status || '待处理' }}
<uni-icons type="down" size="14" color="#FF9800" style="margin-left: 4rpx;"></uni-icons>
</text>
</view>
<text
v-else
class="todo-status"
:class="getTodoStatusTextClass(item.status)">
{{ item.status || '待处理' }}
</text>
</view>
<view class="todo-content" v-if="item.todoContent || item.content || item.description">
<text>{{ item.todoContent || item.content || item.description }}</text>
</view>
<view class="todo-meta">
<text class="todo-meta-item" v-if="item.ownerName">所属人{{ item.ownerName }}</text>
<text class="todo-meta-item" v-if="item.ownerPhone">电话{{ item.ownerPhone }}</text>
<text class="todo-meta-item" v-if="item.PendingDate || item.pendingDate">待办{{ formatDateTime(item.PendingDate || item.pendingDate) }}</text>
</view>
</view>
</view>
<view class="empty-detail" v-else>
<text>暂无待办事项</text>
</view>
</scroll-view>
<!-- 下拉菜单移到modal层级避免被遮挡 -->
<view
class="todo-menu-mask"
v-if="activeTodoMenuIndex !== null"
@click="closeTodoMenu">
</view>
<view
class="todo-menu-fixed"
v-if="activeTodoMenuIndex !== null"
:style="todoMenuStyle"
@click.stop>
<view class="todo-menu-item" @click="handleTodoComplete(currentTodoItem, activeTodoMenuIndex)">
<text>处理完成</text>
</view>
<view class="todo-menu-item" @click="handleTodoDelay(currentTodoItem, activeTodoMenuIndex)">
<text>延期处理</text>
</view>
</view>
<!-- 章节概要内容 -->
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab === 'chapter'">
<view class="chapter-summary-list" v-if="chapterSummaryList.length > 0">
<view
class="chapter-summary-item"
v-for="(item, index) in chapterSummaryList"
:key="index">
<view class="chapter-summary-title">
<text>{{ item.title || `章节${index + 1}` }}</text>
</view>
<view class="chapter-summary-section" v-if="item.customerConcerns">
<view class="chapter-summary-label">
<text>客户疑虑</text>
</view>
<view class="chapter-summary-content">
<text>{{ item.customerConcerns }}</text>
</view>
</view>
<view class="chapter-summary-section" v-if="item.answer">
<view class="chapter-summary-label">
<text>回答</text>
</view>
<view class="chapter-summary-content">
<text>{{ item.answer }}</text>
</view>
</view>
</view>
</view>
<view class="empty-detail" v-else>
<text>暂无章节概要内容</text>
</view>
</scroll-view>
<!-- 其他标签页内容 -->
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab !== 'summary' && activeDetailTab !== 'performance' && activeDetailTab !== 'todo' && activeDetailTab !== 'chapter'">
<view class="empty-detail">
<text>{{ getDetailTabName(activeDetailTab) }}内容</text>
</view>
</scroll-view>
<!-- 底部客户信息 -->
<view class="modal-footer" @click="viewCustomer">
<uni-icons type="person" size="20" color="#666"></uni-icons>
<text class="footer-label">客户:</text>
<text class="footer-customer">{{ currentServiceDetail.customerName }}</text>
<uni-icons type="right" size="16" color="#999"></uni-icons>
</view>
</view>
</uni-popup>
</view>
</template>
<script>
// #ifdef APP
import statusBar from "@/uni_modules/uni-nav-bar/components/uni-nav-bar/uni-status-bar";
// #endif
import CustomerList from './components/CustomerList.vue';
import ServiceList from './components/ServiceList.vue';
import CustomerProfileModal from './components/CustomerProfileModal.vue';
import { initCustomerProfile, initSummaryList, initPerformanceData } from './utils/dataInit.js';
import { getApiUrl, API_BASE_URL } from '@/common/config.js';
export default {
// #ifdef APP
components: {
statusBar
},
// #endif
components: {
CustomerList,
ServiceList,
CustomerProfileModal
},
data() {
return {
activeTab: 'customer',
activeDetailTab: 'summary',
isPlaying: false,
currentServiceDetail: {
title: '',
date: '',
duration: '',
description: '',
customerName: ''
},
currentCustomerProfile: {
name: '',
intent: '',
serviceCount: 0,
lastService: '',
phone: '',
serviceStaff: '',
wechatStatus: '',
address: '',
source: '',
followStage: '',
systemTags: [],
customTags: [],
intentDescription: ''
},
summaryList: [],
furnitureAnalysisList: [], // 家具意向分析数据
productCategoryList: [], // 产品类别列表
activeProductCategory: '', // 当前选中的产品类别
productRequirementMap: {}, // 产品需求映射 {categoryKey: {rows: [...]}}
summarySentence: '', // 一句话总结
dealKeyPoints: '', // 成交关键点
performanceData: {
totalScore: 81,
categories: []
},
serviceList: [],
serviceListTotal: 0,
serviceListPage: {
current: 1,
size: 10
},
todoList: [], // 待办事项列表
todoListPage: {
current: 1,
size: 10
},
todoListTotal: 0, // 待办事项总数
activeTodoMenuIndex: null, // 当前打开的下拉菜单索引
todoMenuStyle: {}, // 下拉菜单样式用于fixed定位
currentTodoItem: null, // 当前操作的待办事项
chapterSummaryList: [], // 章节概要列表包含summary1, summary2, summary3
customerList: [
{
name: '李女士',
intent: '高',
serviceStaff: '三多',
serviceCount: 3,
lastService: '2025-09-10',
tags: [
{ text: '关注优惠活动', color: 'orange' },
{ text: '关注颜色搭配', color: 'orange' },
{ text: '夫妻两一起来', color: 'orange' },
{ text: '新房装修', color: 'blue' },
{ 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' }
]
}
]
}
},
computed: {
// 当前选中的产品需求
currentProductRequirement() {
if (!this.activeProductCategory) return null;
return this.productRequirementMap[this.activeProductCategory] || null;
}
},
onLoad() {
this.loadServiceList();
},
methods: {
goBack() {
uni.navigateBack();
},
switchTab(tab) {
this.activeTab = tab;
// 切换到服务记录标签页时,如果列表为空则加载数据
if (tab === 'service' && this.serviceList.length === 0) {
this.loadServiceList();
}
},
// 刷新服务记录列表
refreshServiceList(filterParams = {}) {
// 重置分页到第一页
this.serviceListPage.current = 1;
// 重新加载数据,传递查询参数
this.loadServiceList(filterParams);
},
// 服务记录列表到底部加载更多
onServiceListReachBottom() {
// 如果已经加载全部数据,则不再请求
if (this.serviceList.length >= this.serviceListTotal) {
return;
}
// 下一页
this.serviceListPage.current += 1;
this.loadServiceList();
},
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();
}
// 如果当前标签页是章节概要,加载章节概要数据
if (this.activeDetailTab === 'chapter') {
this.loadChapterSummary();
}
// 如果当前标签页是服务表现,加载服务表现数据
if (this.activeDetailTab === 'performance') {
this.loadPerformanceData();
}
this.$refs.serviceDetailPopup.open();
},
closeDetail() {
this.$refs.serviceDetailPopup.close();
},
shareDetail() {
// 分享详情
uni.showToast({
title: '分享功能',
icon: 'none'
});
},
togglePlay() {
this.isPlaying = !this.isPlaying;
},
switchDetailTab(tab) {
this.activeDetailTab = tab;
// 切换到智能待办标签页时,加载待办事项数据
if (tab === 'todo') {
this.loadTodoList();
}
// 切换到章节概要标签页时,加载章节概要数据
if (tab === 'chapter') {
this.loadChapterSummary();
}
// 切换到服务表现标签页时,加载服务表现数据
if (tab === 'performance') {
this.loadPerformanceData();
}
},
getDetailTabName(tab) {
const names = {
'todo': '智能待办',
'chapter': '章节概要',
'performance': '服务表现'
};
return names[tab] || '';
},
initSummaryList(item) {
this.summaryList = initSummaryList(item);
this.performanceData = initPerformanceData();
},
// 加载家具意向分析数据
async loadFurnitureAnalysis(item) {
try {
// 获取parentId从rawData中获取id
const parentId = item.rawData?.id || item.id;
if (!parentId) {
console.warn('无法获取parentId');
this.furnitureAnalysisList = [];
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 = [];
}
},
// 用家具意向分析数据更新会话总结
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) || '暂无信息'
}));
},
toggleCategory(index) {
this.performanceData.categories[index].expanded = !this.performanceData.categories[index].expanded;
},
toggleItem(categoryIndex, itemIndex) {
// 可以在这里添加展开子项详情的逻辑
console.log('查看子项详情', categoryIndex, itemIndex);
},
viewSummaryDetail(item) {
// 查看总结详情
console.log('查看总结详情', item);
},
viewCustomer() {
// 查看客户详情
this.closeDetail();
this.switchTab('customer');
},
viewCustomerDetail(item) {
// 查看客户详情
this.currentCustomerProfile = initCustomerProfile(item);
this.$refs.customerProfileModal.open();
},
// 加载待办事项列表
async loadTodoList(params = {}) {
try {
// 获取parentId从当前服务记录的rawData中获取id
const parentId = this.currentServiceDetail.rawData?.id || this.currentServiceDetail.id;
if (!parentId) {
console.warn('无法获取parentId无法加载待办事项');
this.todoList = [];
this.todoListTotal = 0;
return;
}
const queryParams = {
current: this.todoListPage.current,
size: this.todoListPage.size,
parentId: parentId
};
// 添加其他查询参数
if (params.status) {
queryParams.status = params.status;
}
if (params.todoTitle) {
queryParams.todoTitle = params.todoTitle;
}
if (params.ownerName) {
queryParams.ownerName = params.ownerName;
}
if (params.ownerPhone) {
queryParams.ownerPhone = params.ownerPhone;
}
if (params.createStartTime) {
queryParams.createStartTime = params.createStartTime;
}
if (params.createEndTime) {
queryParams.createEndTime = params.createEndTime;
}
if (params.updateStartTime) {
queryParams.updateStartTime = params.updateStartTime;
}
if (params.updateEndTime) {
queryParams.updateEndTime = params.updateEndTime;
}
// 构建查询参数
const queryString = Object.keys(queryParams)
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const baseUrl = getApiUrl('/api/todoItem/list');
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
const res = await uni.request({
url: url,
method: 'GET',
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
this.todoList = res.data.data || [];
this.todoListTotal = res.data.total || 0;
} else {
console.warn('获取待办事项失败:', res.data?.message || '未知错误');
this.todoList = [];
this.todoListTotal = 0;
uni.showToast({
title: res.data?.message || '获取待办事项失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取待办事项失败:', error);
this.todoList = [];
this.todoListTotal = 0;
uni.showToast({
title: `获取待办事项失败: ${error.errMsg || error.message || '未知错误'}`,
icon: 'none',
duration: 3000
});
}
},
// 获取待办事项状态样式类
getTodoStatusClass(status) {
if (!status) return 'status-pending';
if (status.includes('完成') || status === '已完成') {
return 'status-completed';
}
if (status.includes('进行') || status === '进行中') {
return 'status-processing';
}
if (status.includes('待') || status === '待处理') {
return 'status-pending';
}
return 'status-pending';
},
// 获取待办事项状态文本样式类
getTodoStatusTextClass(status) {
if (!status) return 'status-text-pending';
if (status.includes('完成') || status === '已完成') {
return 'status-text-completed';
}
if (status.includes('进行') || status === '进行中') {
return 'status-text-processing';
}
if (status.includes('待') || status === '待处理') {
return 'status-text-pending';
}
return 'status-text-pending';
},
// 格式化日期时间
formatDateTime(dateTimeStr) {
if (!dateTimeStr) return '';
try {
const date = new Date(dateTimeStr);
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');
return `${year}-${month}-${day} ${hours}:${minutes}`;
} catch (error) {
console.warn('格式化日期时间失败:', error);
return dateTimeStr;
}
},
// 判断待办事项是否为待处理状态
isTodoPending(status) {
if (!status) return true;
return status.includes('待') || status === '待处理' || status === '待处理';
},
// 切换待办事项下拉菜单
toggleTodoMenu(index, event) {
if (this.activeTodoMenuIndex === index) {
this.activeTodoMenuIndex = null;
this.currentTodoItem = null;
this.todoMenuStyle = {};
} else {
this.activeTodoMenuIndex = index;
this.currentTodoItem = this.todoList[index];
// 计算下拉菜单位置
this.$nextTick(() => {
const query = uni.createSelectorQuery().in(this);
query.select(`#todo-status-${index}`).boundingClientRect((rect) => {
if (rect) {
// 获取窗口信息
uni.getSystemInfo({
success: (res) => {
// 计算下拉菜单位置:在状态标签下方,右对齐
// rpx转px: 1rpx = 屏幕宽度 / 750
const rpxToPx = res.windowWidth / 750;
const menuWidth = 200 * rpxToPx; // 菜单宽度
const right = res.windowWidth - rect.right;
const top = rect.bottom + 4;
this.todoMenuStyle = {
right: `${right}px`,
top: `${top}px`,
width: `${menuWidth}px`
};
}
});
}
}).exec();
});
}
},
// 关闭待办事项下拉菜单
closeTodoMenu() {
this.activeTodoMenuIndex = null;
this.currentTodoItem = null;
this.todoMenuStyle = {};
},
// 处理完成待办事项
async handleTodoComplete(item, index) {
this.closeTodoMenu();
const todoId = item.id;
if (!todoId) {
uni.showToast({
title: '待办事项ID不存在',
icon: 'none'
});
return;
}
try {
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const url = getApiUrl('/api/todoItem/updateTodoStatus');
const res = await uni.request({
url: url,
method: 'POST',
data: {
id: todoId
},
timeout: 10000,
header: {
'Content-Type': 'application/json'
}
});
if (res.statusCode === 200 && res.data && res.data.success) {
uni.showToast({
title: '处理完成',
icon: 'success'
});
// 刷新待办事项列表
await this.loadTodoList();
} else {
uni.showToast({
title: res.data?.message || '处理失败',
icon: 'none'
});
}
} catch (error) {
console.error('处理待办事项失败:', error);
uni.showToast({
title: `处理失败: ${error.errMsg || error.message || '未知错误'}`,
icon: 'none',
duration: 3000
});
}
},
// 延期处理待办事项
async handleTodoDelay(item, index) {
this.closeTodoMenu();
const todoId = item.id;
if (!todoId) {
uni.showToast({
title: '待办事项ID不存在',
icon: 'none'
});
return;
}
try {
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const url = getApiUrl('/api/todoItem/delayProcessTodo');
const res = await uni.request({
url: url,
method: 'POST',
data: {
id: todoId
},
timeout: 10000,
header: {
'Content-Type': 'application/json'
}
});
if (res.statusCode === 200 && res.data && res.data.success) {
uni.showToast({
title: '已延期3天',
icon: 'success'
});
// 刷新待办事项列表
await this.loadTodoList();
} else {
uni.showToast({
title: res.data?.message || '延期失败',
icon: 'none'
});
}
} catch (error) {
console.error('延期待办事项失败:', error);
uni.showToast({
title: `延期失败: ${error.errMsg || error.message || '未知错误'}`,
icon: 'none',
duration: 3000
});
}
},
onCustomerProfileClose() {
// 客户档案关闭回调
},
// 加载章节概要数据
async loadChapterSummary() {
try {
// 获取parentId从当前服务记录的rawData中获取id
const parentId = this.currentServiceDetail.rawData?.id || this.currentServiceDetail.id;
if (!parentId) {
console.warn('无法获取parentId无法加载章节概要');
this.chapterSummaryList = [];
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) {
const data = res.data.data || [];
// 构建章节概要列表提取summary1, summary2, summary3
const summaryList = [];
// 如果返回的是数组,取第一个元素
const item = Array.isArray(data) && data.length > 0 ? data[0] : data;
if (item) {
// 解析summary1JSON字符串
if (item.summary1) {
try {
const summary1Data = typeof item.summary1 === 'string'
? JSON.parse(item.summary1)
: item.summary1;
if (summary1Data && (summary1Data.customerConcerns || summary1Data.answer)) {
summaryList.push({
title: '章节1',
customerConcerns: summary1Data.customerConcerns || '',
answer: summary1Data.answer || ''
});
}
} catch (error) {
console.warn('解析summary1失败:', error);
}
}
// 解析summary2JSON字符串
if (item.summary2) {
try {
const summary2Data = typeof item.summary2 === 'string'
? JSON.parse(item.summary2)
: item.summary2;
if (summary2Data && (summary2Data.customerConcerns || summary2Data.answer)) {
summaryList.push({
title: '章节2',
customerConcerns: summary2Data.customerConcerns || '',
answer: summary2Data.answer || ''
});
}
} catch (error) {
console.warn('解析summary2失败:', error);
}
}
// 解析summary3JSON字符串
if (item.summary3) {
try {
const summary3Data = typeof item.summary3 === 'string'
? JSON.parse(item.summary3)
: item.summary3;
if (summary3Data && (summary3Data.customerConcerns || summary3Data.answer)) {
summaryList.push({
title: '章节3',
customerConcerns: summary3Data.customerConcerns || '',
answer: summary3Data.answer || ''
});
}
} catch (error) {
console.warn('解析summary3失败:', error);
}
}
}
this.chapterSummaryList = summaryList;
} else {
console.warn('获取章节概要失败:', res.data?.message || '未知错误');
this.chapterSummaryList = [];
}
} catch (error) {
console.error('获取章节概要失败:', error);
this.chapterSummaryList = [];
}
},
// 加载服务表现数据
async loadPerformanceData() {
try {
// 获取parentId从当前服务记录的rawData中获取id
const parentId = this.currentServiceDetail.rawData?.id || this.currentServiceDetail.id;
if (!parentId) {
console.warn('无法获取parentId无法加载服务表现数据');
// 使用默认数据
this.performanceData = initPerformanceData();
return;
}
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const url = `${getApiUrl('/api/audioTextAnalysisSop/listByParentId')}?parentId=${parentId}`;
const res = await uni.request({
url: url,
method: 'GET',
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
const data = res.data.data;
// 如果返回的是数组,取第一个元素
const sopData = Array.isArray(data) && data.length > 0 ? data[0] : data;
if (sopData) {
// 将后端数据转换为前端需要的格式
this.performanceData = this.convertSopDataToPerformanceData(sopData);
} else {
console.warn('服务表现数据为空');
// 使用默认数据
this.performanceData = initPerformanceData();
}
} else {
console.warn('获取服务表现数据失败:', res.data?.message || '未知错误');
// 使用默认数据
this.performanceData = initPerformanceData();
}
} catch (error) {
console.error('获取服务表现数据失败:', error);
// 使用默认数据
this.performanceData = initPerformanceData();
}
},
// 将后端SOP数据转换为前端performanceData格式
convertSopDataToPerformanceData(sopData) {
// 判断分数是否完成分数大于等于60分认为完成
const isCompleted = (score) => score !== null && score !== undefined && score >= 60;
// 收集所有子项的分数用于计算总分
const allScores = [
sopData.greetingIceBreaking,
sopData.brandIntroduction,
sopData.goldenThreeQuestions,
sopData.needsGuidance,
sopData.serviceProgression,
sopData.reassuranceHandbook,
sopData.threeLevelPricing,
sopData.objectionHandling,
sopData.activityImplantation,
sopData.closingCooperation,
sopData.proactiveWechatAdd,
sopData.politeFarewell
].filter(score => score !== null && score !== undefined);
// 计算总分优先使用后端返回的comprehensiveScore如果没有则计算所有子项的平均分
let totalScore = sopData.comprehensiveScore || sopData.comprehensive_score;
if (totalScore === null || totalScore === undefined) {
// 如果后端没有返回总分,则计算所有子项的平均分
if (allScores.length > 0) {
const sum = allScores.reduce((acc, score) => acc + score, 0);
totalScore = Math.round(sum / allScores.length);
} else {
totalScore = 0;
}
}
// 构建分类和子项数据
const categories = [
{
name: '迎宾探寻',
completed: isCompleted(sopData.greetingIceBreaking) || isCompleted(sopData.brandIntroduction),
expanded: true,
showBanner: false,
showAudio: false,
items: [
{
name: '破冰',
completed: isCompleted(sopData.greetingIceBreaking),
points: sopData.greetingIceBreaking || 0
},
{
name: '品牌介绍',
completed: isCompleted(sopData.brandIntroduction),
points: sopData.brandIntroduction || 0
}
]
},
{
name: '展厅体验',
completed: isCompleted(sopData.goldenThreeQuestions) || isCompleted(sopData.needsGuidance),
expanded: true,
showBanner: false,
showAudio: false,
items: [
{
name: '黄金三问',
completed: isCompleted(sopData.goldenThreeQuestions),
points: sopData.goldenThreeQuestions || 0
},
{
name: '需求引导',
completed: isCompleted(sopData.needsGuidance),
points: sopData.needsGuidance || 0
}
]
},
{
name: '落座规划',
completed: isCompleted(sopData.serviceProgression) || isCompleted(sopData.reassuranceHandbook),
expanded: true,
showBanner: false,
showAudio: false,
items: [
{
name: '服务递进',
completed: isCompleted(sopData.serviceProgression),
points: sopData.serviceProgression || 0
},
{
name: 'SGS放心手册',
completed: isCompleted(sopData.reassuranceHandbook),
points: sopData.reassuranceHandbook || 0
}
]
},
{
name: '方案报价',
completed: isCompleted(sopData.threeLevelPricing),
expanded: true,
showBanner: false,
showAudio: false,
items: [
{
name: '三级报价',
completed: isCompleted(sopData.threeLevelPricing),
points: sopData.threeLevelPricing || 0
}
]
},
{
name: '促单成交',
completed: isCompleted(sopData.objectionHandling),
expanded: true,
showBanner: false,
showAudio: false,
items: [
{
name: '解答异议',
completed: isCompleted(sopData.objectionHandling),
points: sopData.objectionHandling || 0
}
]
},
{
name: '送客跟进',
completed: isCompleted(sopData.activityImplantation) ||
isCompleted(sopData.closingCooperation) ||
isCompleted(sopData.proactiveWechatAdd) ||
isCompleted(sopData.politeFarewell),
expanded: true,
showBanner: false,
showAudio: false,
items: [
{
name: '活动植入',
completed: isCompleted(sopData.activityImplantation),
points: sopData.activityImplantation || 0
},
{
name: '压单配合',
completed: isCompleted(sopData.closingCooperation),
points: sopData.closingCooperation || 0
},
{
name: '主动添加微信',
completed: isCompleted(sopData.proactiveWechatAdd),
points: sopData.proactiveWechatAdd || 0
},
{
name: '邀约量尺时间',
completed: false,
points: 0
},
{
name: '礼貌道别',
completed: isCompleted(sopData.politeFarewell),
points: sopData.politeFarewell || 0
}
]
}
];
return {
totalScore: totalScore,
categories: categories
};
},
// 加载服务记录列表
async loadServiceList(params = {}) {
try {
const queryParams = {
current: this.serviceListPage.current,
size: this.serviceListPage.size
};
// 添加查询参数:所属销售姓名和客户姓名
if (params.salesName) {
queryParams.salesName = params.salesName;
}
if (params.customerName) {
queryParams.customerName = params.customerName;
}
// 构建查询参数
const queryString = Object.keys(queryParams)
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
.map(key => `${key}=${encodeURIComponent(queryParams[key])}`)
.join('&');
// 统一使用 getApiUrl根据 env.js 配置自动切换环境
const baseUrl = getApiUrl('/api/audioManagement/list');
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
const res = await uni.request({
url: url,
method: 'GET',
timeout: 10000
});
if (res.statusCode === 200 && res.data && res.data.success) {
// 转换后端数据为页面需要的格式
const mappedList = res.data.data.map(item => {
// 格式化时间:将 ISO 格式转换为 YYYY-MM-DD HH:mm:ss
let formattedTime = '';
if (item.createTime) {
const date = new Date(item.createTime);
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');
formattedTime = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
}
return {
id: item.id,
title: `${item.salesName || ''}的服务记录`,
date: formattedTime ? `时间:${formattedTime}` : '',
description: item.summary || '',
customerName: item.customerName || '',
// 保留原始数据,用于详情页
rawData: item
};
});
// 分页:第一页或刷新时重置,其他情况追加
if (this.serviceListPage.current === 1) {
this.serviceList = mappedList;
} else {
this.serviceList = this.serviceList.concat(mappedList);
}
this.serviceListTotal = res.data.total || 0;
} else {
uni.showToast({
title: res.data.message || '获取服务记录失败',
icon: 'none'
});
}
} catch (error) {
console.error('获取服务记录失败:', error);
uni.showToast({
title: `获取服务记录失败: ${error.errMsg || error.message || '未知错误'}`,
icon: 'none',
duration: 3000
});
}
},
}
}
</script>
<style>
page {
background-color: #F5F5F5;
}
.page {
min-height: 100vh;
background-color: #F5F5F5;
}
.content {
padding-top: 88rpx; /* 导航栏高度 */
}
/* 标签页 */
.tabs {
display: flex;
background-color: #FFFFFF;
border-bottom: 1px solid #E0E0E0;
padding: 0 32rpx;
}
.tab-item {
padding: 24rpx 32rpx;
position: relative;
}
.tab-item text {
font-size: 30rpx;
color: #666;
}
.tab-item.active text {
color: #333;
font-weight: 500;
}
.tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 32rpx;
right: 32rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
/* 列表头部 */
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 24rpx 32rpx;
background-color: #FFFFFF;
border-bottom: 1px solid #F0F0F0;
}
.total {
font-size: 28rpx;
color: #666;
}
.filter-btn {
padding: 8rpx 16rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
}
.filter-btn text {
font-size: 24rpx;
color: #666;
}
/* 服务记录列表 */
.service-list {
height: calc(100vh - 420rpx);
background-color: #FFFFFF;
}
.service-item {
padding: 32rpx;
border-bottom: 1px solid #F0F0F0;
background-color: #FFFFFF;
}
.service-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.service-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
}
.service-actions {
display: flex;
align-items: center;
}
.archive-text {
font-size: 26rpx;
color: #999;
margin-right: 8rpx;
}
.service-meta {
display: flex;
align-items: center;
margin-bottom: 16rpx;
}
.service-date {
font-size: 24rpx;
color: #999;
margin-right: 16rpx;
}
.service-duration {
font-size: 24rpx;
color: #999;
}
.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;
}
/* 服务记录详情弹窗 */
.service-detail-modal {
background-color: #FFFFFF;
border-radius: 32rpx 32rpx 0 0;
max-height: 90vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
.modal-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
border-bottom: 1px solid #F0F0F0;
}
.modal-close,
.modal-share {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
}
.modal-title {
font-size: 36rpx;
font-weight: 600;
color: #333;
flex: 1;
text-align: center;
}
.modal-date {
padding: 16rpx 32rpx;
}
.modal-date text {
font-size: 24rpx;
color: #999;
}
.modal-banner {
background-color: #E3F2FD;
padding: 24rpx 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin: 0 32rpx 24rpx;
border-radius: 16rpx;
}
.modal-banner .banner-left {
display: flex;
align-items: center;
flex: 1;
}
.modal-banner .banner-icon {
width: 40rpx;
height: 40rpx;
background-color: #FFFFFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
}
.modal-banner .banner-text {
font-size: 26rpx;
color: #1976D2;
flex: 1;
}
.modal-banner .banner-right {
font-size: 28rpx;
color: #1976D2;
font-weight: 500;
margin-left: 16rpx;
}
/* 音频播放器 */
.audio-player {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
margin: 0 32rpx 24rpx;
background-color: #F5F5F5;
border-radius: 16rpx;
}
.play-btn {
width: 48rpx;
height: 48rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16rpx;
}
.progress-bar {
flex: 1;
height: 4rpx;
background-color: #E0E0E0;
border-radius: 2rpx;
margin: 0 16rpx;
position: relative;
}
.progress-line {
width: 30%;
height: 100%;
background-color: #007AFF;
border-radius: 2rpx;
position: relative;
}
.progress-dot {
position: absolute;
right: -8rpx;
top: 50%;
transform: translateY(-50%);
width: 16rpx;
height: 16rpx;
background-color: #007AFF;
border-radius: 50%;
border: 2rpx solid #FFFFFF;
}
.audio-time {
font-size: 24rpx;
color: #666;
margin-right: 16rpx;
min-width: 200rpx;
}
.speed-btn {
padding: 8rpx 16rpx;
background-color: #FFFFFF;
border-radius: 8rpx;
border: 1px solid #E0E0E0;
}
.speed-btn text {
font-size: 24rpx;
color: #333;
}
/* 详情标签页 */
.detail-tabs {
display: flex;
background-color: #FFFFFF;
border-bottom: 1px solid #E0E0E0;
padding: 0 32rpx;
overflow-x: auto;
position: sticky;
top: 0;
z-index: 10;
flex-shrink: 0;
}
.detail-tab-item {
padding: 24rpx 16rpx;
position: relative;
white-space: nowrap;
margin-right: 32rpx;
}
.detail-tab-item text {
font-size: 28rpx;
color: #666;
}
.detail-tab-item.active text {
color: #333;
font-weight: 500;
}
.detail-tab-item.active::after {
content: '';
position: absolute;
bottom: 0;
left: 16rpx;
right: 16rpx;
height: 4rpx;
background-color: #007AFF;
border-radius: 2rpx;
}
/* 详情内容 */
.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%);
}
.empty-detail {
padding: 80rpx 0;
text-align: center;
}
.empty-detail text {
font-size: 28rpx;
color: #999;
}
/* 底部客户信息 */
.modal-footer {
display: flex;
align-items: center;
padding: 24rpx 32rpx;
border-top: 1px solid #F0F0F0;
background-color: #FFFFFF;
}
.footer-label {
font-size: 28rpx;
color: #666;
margin: 0 8rpx 0 12rpx;
}
.footer-customer {
font-size: 28rpx;
color: #1976D2;
flex: 1;
}
/* 服务表现样式 */
.performance-score {
text-align: center;
padding: 40rpx 32rpx;
background-color: #FFFFFF;
border-bottom: 1px solid #F0F0F0;
}
.score-label {
display: block;
font-size: 28rpx;
color: #666;
margin-bottom: 16rpx;
}
.score-value {
display: block;
font-size: 72rpx;
font-weight: 600;
color: #007AFF;
}
.performance-category {
background-color: #FFFFFF;
border-bottom: 1px solid #F0F0F0;
}
.category-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 32rpx;
}
.category-left {
display: flex;
align-items: center;
flex: 1;
}
.category-bullet {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 16rpx;
}
.bullet-blue {
background-color: #007AFF;
}
.bullet-gray {
background-color: #999;
}
.category-name {
font-size: 30rpx;
color: #333;
font-weight: 500;
}
.category-content {
padding: 0 32rpx 32rpx;
}
.performance-banner {
background-color: #E3F2FD;
padding: 24rpx 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 24rpx;
border-radius: 16rpx;
}
.performance-banner .banner-left {
display: flex;
align-items: center;
flex: 1;
}
.performance-banner .banner-icon {
width: 40rpx;
height: 40rpx;
background-color: #FFFFFF;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-right: 12rpx;
}
.performance-banner .banner-text {
font-size: 26rpx;
color: #1976D2;
flex: 1;
}
.performance-banner .banner-right {
font-size: 28rpx;
color: #1976D2;
font-weight: 500;
margin-left: 16rpx;
}
.performance-audio {
margin-bottom: 24rpx;
}
.performance-audio .audio-player {
margin: 0;
}
.performance-item {
margin-bottom: 16rpx;
}
.performance-item.item-completed {
background-color: #E8F5E9;
border-radius: 12rpx;
padding: 16rpx;
}
.item-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16rpx 0;
}
.item-left {
display: flex;
align-items: center;
flex: 1;
flex-wrap: wrap;
gap: 12rpx;
}
.item-name {
font-size: 28rpx;
color: #333;
margin-right: 16rpx;
}
.item-status {
font-size: 24rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
}
.status-completed {
background-color: #4CAF50;
color: #FFFFFF;
}
.status-missing {
background-color: #F5F5F5;
color: #999;
}
.item-points {
font-size: 24rpx;
color: #007AFF;
font-weight: 500;
margin-left: 8rpx;
}
/* 产品类别标签页 */
.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;
}
/* 待办事项列表 */
.todo-list {
padding: 0;
}
.todo-item {
padding: 32rpx;
border-bottom: 1px solid #F0F0F0;
background-color: #FFFFFF;
}
.todo-item:last-child {
border-bottom: none;
}
.todo-item.todo-completed {
background-color: #F5F5F5;
opacity: 0.8;
}
.todo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
}
.todo-title-wrapper {
display: flex;
align-items: center;
flex: 1;
}
.todo-status-dot {
width: 16rpx;
height: 16rpx;
border-radius: 50%;
margin-right: 12rpx;
flex-shrink: 0;
}
.todo-status-dot.status-pending {
background-color: #FF9800;
}
.todo-status-dot.status-processing {
background-color: #2196F3;
}
.todo-status-dot.status-completed {
background-color: #4CAF50;
}
.todo-title {
font-size: 32rpx;
font-weight: 500;
color: #333;
flex: 1;
}
.todo-status {
font-size: 24rpx;
padding: 6rpx 16rpx;
border-radius: 12rpx;
white-space: nowrap;
}
.todo-status.status-text-pending {
background-color: #FFF3E0;
color: #FF9800;
}
.todo-status.status-text-processing {
background-color: #E3F2FD;
color: #2196F3;
}
.todo-status.status-text-completed {
background-color: #E8F5E9;
color: #4CAF50;
}
.todo-status-wrapper {
position: relative;
z-index: 10;
}
.todo-status-clickable {
display: flex;
align-items: center;
}
.todo-item.todo-clickable .todo-status-clickable {
position: relative;
}
.todo-menu-fixed {
position: fixed;
background-color: #FFFFFF;
border-radius: 8rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.15);
z-index: 10000;
overflow: hidden;
}
.todo-menu-item {
padding: 24rpx 32rpx;
border-bottom: 1px solid #F0F0F0;
transition: background-color 0.2s;
}
.todo-menu-fixed .todo-menu-item:last-child {
border-bottom: none;
}
.todo-menu-item:active {
background-color: #F5F5F5;
}
.todo-menu-item text {
font-size: 28rpx;
color: #333;
}
.todo-menu-mask {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.01);
z-index: 9999;
}
.todo-content {
margin-bottom: 16rpx;
padding: 16rpx;
background-color: #F5F5F5;
border-radius: 8rpx;
}
.todo-content text {
font-size: 28rpx;
color: #666;
line-height: 1.6;
}
.todo-meta {
display: flex;
flex-wrap: wrap;
gap: 16rpx;
}
.todo-meta-item {
font-size: 24rpx;
color: #999;
}
/* 章节概要样式 */
.chapter-summary-list {
padding: 0;
}
.chapter-summary-item {
background-color: #FFFFFF;
border-radius: 16rpx;
padding: 32rpx;
margin-bottom: 24rpx;
border: 1px solid #F0F0F0;
}
.chapter-summary-item:last-child {
margin-bottom: 0;
}
.chapter-summary-title {
margin-bottom: 24rpx;
padding-bottom: 16rpx;
border-bottom: 2px solid #007AFF;
}
.chapter-summary-title text {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.chapter-summary-section {
margin-bottom: 24rpx;
}
.chapter-summary-section:last-child {
margin-bottom: 0;
}
.chapter-summary-label {
margin-bottom: 12rpx;
}
.chapter-summary-label text {
font-size: 28rpx;
font-weight: 500;
color: #666;
}
.chapter-summary-content {
background-color: #F5F5F5;
border-radius: 12rpx;
padding: 20rpx;
}
.chapter-summary-content text {
font-size: 28rpx;
color: #333;
line-height: 1.8;
}
</style>