2639 lines
71 KiB
Vue
2639 lines
71 KiB
Vue
<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) {
|
||
// 解析summary1(JSON字符串)
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 解析summary2(JSON字符串)
|
||
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);
|
||
}
|
||
}
|
||
|
||
// 解析summary3(JSON字符串)
|
||
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>
|
||
|