3497 lines
95 KiB
Vue
3497 lines
95 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"
|
||
@menuAction="handleMenuAction"
|
||
@updateItem="handleUpdateItem" />
|
||
|
||
<!-- 客户列表 -->
|
||
<CustomerList
|
||
v-if="activeTab === 'customer'"
|
||
@itemClick="viewCustomerDetail" />
|
||
</view>
|
||
|
||
<!-- 客户档案弹窗 -->
|
||
<CustomerProfileModal
|
||
ref="customerProfileModal"
|
||
:profile="currentCustomerProfile"
|
||
@close="onCustomerProfileClose" />
|
||
|
||
<!-- 录音列表弹窗 -->
|
||
<uni-popup ref="audioListPopup" type="bottom" :mask-click="false">
|
||
<view class="audio-list-modal">
|
||
<!-- 顶部栏 -->
|
||
<view class="modal-header">
|
||
<view class="modal-close" @click="closeAudioList">
|
||
<uni-icons type="close" size="24" color="#333"></uni-icons>
|
||
</view>
|
||
<text class="modal-title">录音列表({{ audioSegmentsList.length }})</text>
|
||
<view style="width: 48rpx;"></view>
|
||
</view>
|
||
<!-- 录音列表内容 -->
|
||
<scroll-view class="audio-list-content" scroll-y enable-flex>
|
||
<view
|
||
class="audio-list-item"
|
||
v-for="(item, index) in audioSegmentsList"
|
||
:key="index">
|
||
<view class="audio-item-header">
|
||
<view class="audio-play-btn" @click.stop="toggleAudioPlay(item, index)">
|
||
<text class="audio-play-text" :class="playingAudioId === (item.id || item.segmentId) ? 'audio-play-text--playing' : ''">
|
||
{{ playingAudioId === (item.id || item.segmentId) ? '暂停' : '播放' }}
|
||
</text>
|
||
</view>
|
||
<text class="audio-item-name">{{ formatAudioFileName(item.audioFileOriginalName) }}</text>
|
||
</view>
|
||
<view class="audio-item-info">
|
||
<text class="audio-item-text" v-if="item.recordingText">{{ truncateText(item.recordingText, 30) }}</text>
|
||
<view class="audio-item-transcribe" v-else>
|
||
<view
|
||
class="transcribe-btn"
|
||
:class="{ 'transcribe-btn--loading': transcribingIds.includes(item.id || item.segmentId) }"
|
||
@click.stop="transcribeAudio(item, index)">
|
||
<text v-if="!transcribingIds.includes(item.id || item.segmentId)">转文本</text>
|
||
<text v-else>处理中...</text>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
<view class="audio-item-meta">
|
||
<text class="audio-meta-item" v-if="item.endTime">{{ formatDateTime(item.endTime) }}</text>
|
||
<text class="audio-meta-item" v-if="item.duration">{{ formatDuration(item.duration) }}</text>
|
||
<text class="audio-meta-item" v-if="item.deviceNo">{{ item.deviceNo }}</text>
|
||
</view>
|
||
</view>
|
||
<view class="audio-list-empty" v-if="!audioSegmentsLoading && audioSegmentsList.length === 0">
|
||
<text>暂无录音记录</text>
|
||
</view>
|
||
</scroll-view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 录音文本内容弹框(放在录音列表弹框之后,确保层级更高) -->
|
||
<uni-popup ref="textModalPopup" type="center" :mask-click="true" @change="onTextModalChange" class="text-modal-popup">
|
||
<view class="text-modal">
|
||
<view class="text-modal-header">
|
||
<text class="text-modal-title">录音文本内容</text>
|
||
<view class="text-modal-close" @click="closeTextModal">
|
||
<uni-icons type="close" size="24" color="#333"></uni-icons>
|
||
</view>
|
||
</view>
|
||
<scroll-view class="text-modal-content" scroll-y>
|
||
<text class="text-modal-text">{{ currentRecordingText || '暂无录音文本' }}</text>
|
||
</scroll-view>
|
||
</view>
|
||
</uni-popup>
|
||
|
||
<!-- 服务记录详情弹窗 -->
|
||
<uni-popup ref="serviceDetailPopup" type="bottom" :mask-click="false">
|
||
<view class="service-detail-modal">
|
||
<!-- 顶部栏 -->
|
||
<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>
|
||
|
||
<!-- 会话总结内容 -->
|
||
<ConversationSummaryFurniture
|
||
v-if="activeDetailTab === 'summary'"
|
||
:serviceItem="currentServiceDetail"
|
||
:key="`summary-${currentServiceDetail.id || currentServiceDetail.rawData?.id || Date.now()}`" />
|
||
|
||
<!-- 服务表现内容 -->
|
||
<scroll-view class="detail-content" scroll-y v-if="activeDetailTab === 'performance'">
|
||
<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 ConversationSummaryFurniture from './components/ConversationSummaryFurniture.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,
|
||
ConversationSummaryFurniture
|
||
},
|
||
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
|
||
audioSegmentsList: [], // 录音分段列表
|
||
audioSegmentsLoading: false, // 录音分段加载状态
|
||
playingAudioId: null, // 当前正在播放的录音ID
|
||
innerAudioContext: null, // 音频播放器实例
|
||
currentAudioUrl: null, // 当前音频URL(用于清理Blob URL)
|
||
showTextModal: false, // 是否显示文本内容弹框
|
||
currentRecordingText: '', // 当前显示的录音文本内容
|
||
transcribingIds: [] // 正在转文本的录音ID列表
|
||
}
|
||
},
|
||
computed: {
|
||
// 当前选中的产品需求
|
||
currentProductRequirement() {
|
||
if (!this.activeProductCategory) return null;
|
||
return this.productRequirementMap[this.activeProductCategory] || null;
|
||
}
|
||
},
|
||
onLoad() {
|
||
// 加载服务记录列表
|
||
if (this.activeTab === 'service') {
|
||
this.loadServiceList();
|
||
}
|
||
},
|
||
onUnload() {
|
||
// 组件销毁时,清理音频播放器
|
||
if (this.innerAudioContext) {
|
||
try {
|
||
this.innerAudioContext.stop();
|
||
this.innerAudioContext.destroy();
|
||
} catch (e) {
|
||
console.error('销毁音频播放器失败:', e);
|
||
}
|
||
this.innerAudioContext = null;
|
||
}
|
||
// #ifdef H5
|
||
// 清理Blob URL,释放内存
|
||
if (this.currentAudioUrl && this.currentAudioUrl.startsWith('blob:')) {
|
||
try {
|
||
URL.revokeObjectURL(this.currentAudioUrl);
|
||
} catch (e) {
|
||
console.error('清理Blob URL失败:', e);
|
||
}
|
||
}
|
||
// #endif
|
||
this.currentAudioUrl = null;
|
||
this.playingAudioId = null;
|
||
},
|
||
methods: {
|
||
goBack() {
|
||
uni.navigateBack();
|
||
},
|
||
switchTab(tab) {
|
||
this.activeTab = tab;
|
||
// 切换到服务记录标签页时,如果列表为空则加载数据
|
||
if (tab === 'service' && this.serviceList.length === 0) {
|
||
this.loadServiceList();
|
||
}
|
||
// 客户列表数据由 CustomerList 组件自己管理,不需要在这里加载
|
||
},
|
||
// 刷新服务记录列表
|
||
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.todoList = [];
|
||
this.todoListTotal = 0;
|
||
// 重置章节概要列表
|
||
this.chapterSummaryList = [];
|
||
// 如果当前标签页是智能待办,加载待办事项
|
||
if (this.activeDetailTab === 'todo') {
|
||
this.loadTodoList();
|
||
}
|
||
// 如果当前标签页是章节概要,加载章节概要数据
|
||
if (this.activeDetailTab === 'chapter') {
|
||
this.loadChapterSummary();
|
||
}
|
||
// 如果当前标签页是服务表现,加载服务表现数据
|
||
if (this.activeDetailTab === 'performance') {
|
||
this.loadPerformanceData();
|
||
}
|
||
// 会话总结数据由"会话总结家具"组件自动加载
|
||
this.$refs.serviceDetailPopup.open();
|
||
},
|
||
// 处理菜单操作
|
||
async handleMenuAction({ action, item }) {
|
||
console.log('父组件收到菜单操作:', action, item);
|
||
switch (action) {
|
||
case 'view':
|
||
// 查看详情:打开详情弹窗
|
||
this.viewDetail(item);
|
||
break;
|
||
case 'analysis':
|
||
// AI分析:已经在 ServiceList 组件中处理,这里不需要处理
|
||
break;
|
||
case 'audio':
|
||
// 录音详情:加载录音分段列表
|
||
await this.loadAudioSegments(item);
|
||
break;
|
||
case 'delete':
|
||
// 删除服务记录
|
||
await this.deleteServiceRecord(item);
|
||
break;
|
||
default:
|
||
console.warn('未知的菜单操作:', action);
|
||
}
|
||
},
|
||
// 处理列表项更新
|
||
handleUpdateItem({ recordId, data }) {
|
||
// 在列表中找到对应的项并更新
|
||
const index = this.serviceList.findIndex(serviceItem =>
|
||
(serviceItem.id || serviceItem.rawData?.id) === recordId
|
||
);
|
||
if (index !== -1) {
|
||
this.$set(this.serviceList, index, data);
|
||
}
|
||
},
|
||
// 删除服务记录
|
||
async deleteServiceRecord(item) {
|
||
console.log('删除服务记录,item:', item);
|
||
// 显示确认对话框
|
||
const res = await new Promise((resolve) => {
|
||
uni.showModal({
|
||
title: '确认删除',
|
||
content: '确定要删除这条服务记录吗?删除后无法恢复。',
|
||
confirmText: '删除',
|
||
confirmColor: '#FF3B30',
|
||
cancelText: '取消',
|
||
success: (result) => {
|
||
resolve(result.confirm);
|
||
},
|
||
fail: () => {
|
||
resolve(false);
|
||
}
|
||
});
|
||
});
|
||
|
||
if (!res) {
|
||
return; // 用户取消删除
|
||
}
|
||
|
||
try {
|
||
// 获取要删除的记录ID
|
||
const recordId = item.id || item.rawData?.id;
|
||
if (!recordId) {
|
||
uni.showToast({
|
||
title: '无法获取记录ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 调用删除接口:/api/audioManagement/delete/{id}
|
||
const url = getApiUrl(`/api/audioManagement/delete/${recordId}`);
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
const deleteRes = await uni.request({
|
||
url: url,
|
||
method: 'DELETE',
|
||
header: headers,
|
||
timeout: 10000
|
||
});
|
||
|
||
if (deleteRes.statusCode === 200 && deleteRes.data && deleteRes.data.success) {
|
||
uni.showToast({
|
||
title: '删除成功',
|
||
icon: 'success'
|
||
});
|
||
// 刷新列表
|
||
this.serviceListPage.current = 1;
|
||
this.serviceList = [];
|
||
await this.loadServiceList();
|
||
} else {
|
||
uni.showToast({
|
||
title: deleteRes.data?.message || '删除失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.error('删除服务记录失败:', error);
|
||
uni.showToast({
|
||
title: `删除失败: ${error.errMsg || error.message || '未知错误'}`,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
// 加载录音分段列表
|
||
async loadAudioSegments(item) {
|
||
try {
|
||
// 获取parentId,从item的rawData中获取id,或者直接使用id
|
||
const parentId = item.rawData?.id || item.id;
|
||
if (!parentId) {
|
||
uni.showToast({
|
||
title: '无法获取录音ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
this.audioSegmentsLoading = true;
|
||
this.audioSegmentsList = [];
|
||
|
||
uni.showLoading({
|
||
title: '加载中...'
|
||
});
|
||
|
||
// 调用接口:/api/audioManagementSegments/listByParentId?parentId={parentId}
|
||
const url = `${getApiUrl('/api/audioManagementSegments/listByParentId')}?parentId=${parentId}`;
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
const res = await uni.request({
|
||
url: url,
|
||
method: 'GET',
|
||
header: headers,
|
||
timeout: 10000
|
||
});
|
||
|
||
uni.hideLoading();
|
||
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
const segments = res.data.data || [];
|
||
console.log('录音分段列表:', segments);
|
||
|
||
// 存储分段数据
|
||
this.audioSegmentsList = segments;
|
||
this.audioSegmentsLoading = false;
|
||
|
||
// 打开录音列表弹窗
|
||
this.$refs.audioListPopup.open();
|
||
} else {
|
||
uni.showToast({
|
||
title: res.data?.message || '获取录音分段失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
this.audioSegmentsLoading = false;
|
||
console.error('获取录音分段列表失败:', error);
|
||
uni.showToast({
|
||
title: `获取录音分段失败: ${error.errMsg || error.message || '未知错误'}`,
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
// 关闭录音列表弹窗
|
||
closeAudioList() {
|
||
// 关闭弹窗时,如果正在播放,先停止播放
|
||
if (this.playingAudioId) {
|
||
this.stopAudioPlay({ id: this.playingAudioId });
|
||
}
|
||
this.$refs.audioListPopup.close();
|
||
},
|
||
// 切换录音播放/暂停
|
||
async toggleAudioPlay(item, index) {
|
||
try {
|
||
// 获取录音ID(可能是 id 或 segmentId)
|
||
const audioId = item.id || item.segmentId;
|
||
if (!audioId) {
|
||
console.warn('录音项缺少ID:', item);
|
||
uni.showToast({
|
||
title: '无法获取录音ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
|
||
|
||
// 如果点击的是当前正在播放的录音,则暂停
|
||
if (this.playingAudioId === audioId) {
|
||
await this.stopAudioPlay(item);
|
||
} else {
|
||
// 如果其他录音正在播放,先停止
|
||
if (this.playingAudioId) {
|
||
await this.stopAudioPlay({ id: this.playingAudioId });
|
||
}
|
||
// 显示文本内容弹框
|
||
this.currentRecordingText = item.recordingText || '';
|
||
this.showTextModal = true;
|
||
this.$refs.textModalPopup.open();
|
||
// 开始播放新的录音
|
||
await this.startAudioPlay(item);
|
||
}
|
||
} catch (error) {
|
||
console.error('切换播放状态失败:', error);
|
||
uni.showToast({
|
||
title: '操作失败',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
},
|
||
// 关闭文本内容弹框
|
||
closeTextModal() {
|
||
this.showTextModal = false;
|
||
this.currentRecordingText = '';
|
||
this.$refs.textModalPopup.close();
|
||
},
|
||
// 文本弹框状态变化
|
||
onTextModalChange(e) {
|
||
if (!e.show) {
|
||
// 弹框关闭时,清除状态
|
||
this.showTextModal = false;
|
||
this.currentRecordingText = '';
|
||
}
|
||
},
|
||
// 开始播放录音
|
||
async startAudioPlay(item) {
|
||
try {
|
||
// 获取录音ID(可能是 id 或 segmentId)
|
||
const audioId = item.id || item.segmentId;
|
||
if (!audioId) {
|
||
console.warn('录音项缺少ID:', item);
|
||
uni.showToast({
|
||
title: '无法获取录音ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果已有播放器正在播放,先停止
|
||
if (this.innerAudioContext) {
|
||
this.innerAudioContext.stop();
|
||
this.innerAudioContext.destroy();
|
||
this.innerAudioContext = null;
|
||
}
|
||
|
||
uni.showLoading({
|
||
title: '加载中...'
|
||
});
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求URL
|
||
// 根据后端接口定义:@PostMapping("/playSegmentById") 使用 @PathVariable String id
|
||
// 如果使用 @PathVariable,路径应该是 /playSegmentById/{id}
|
||
// 先尝试路径参数方式:/api/audioManagementSegments/playSegmentById/{id}
|
||
const url = getApiUrl(`/api/audioManagementSegments/playSegmentById/${audioId}`);
|
||
|
||
console.log('请求音频URL:', url);
|
||
console.log('音频ID:', audioId);
|
||
|
||
// 构建请求头(参考其他请求的实现方式)
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
console.log('请求头:', headers);
|
||
|
||
// 由于后端接口是POST方式,且需要传递header,而innerAudioContext无法设置请求头
|
||
// 所以先通过uni.request获取音频数据,然后转换为可播放的URL
|
||
const res = await uni.request({
|
||
url: url,
|
||
method: 'POST',
|
||
header: headers,
|
||
responseType: 'arraybuffer', // 接收二进制数据
|
||
timeout: 30000 // 音频文件可能较大,增加超时时间
|
||
});
|
||
|
||
console.log('响应状态码:', res.statusCode);
|
||
console.log('响应头:', res.header);
|
||
|
||
if (res.statusCode !== 200) {
|
||
console.error('请求失败,状态码:', res.statusCode);
|
||
console.error('响应数据:', res.data);
|
||
// 如果是404,可能是路径不对,尝试其他方式
|
||
if (res.statusCode === 404) {
|
||
throw new Error(`接口路径不存在(404),请检查后端接口路径是否正确。当前URL: ${url}`);
|
||
}
|
||
throw new Error(`请求失败,状态码: ${res.statusCode}`);
|
||
}
|
||
|
||
// 从响应头获取Content-Type,确定音频格式
|
||
// 如果响应头中没有Content-Type,默认使用audio/mpeg
|
||
let audioType = 'audio/mpeg';
|
||
if (res.header && res.header['Content-Type']) {
|
||
audioType = res.header['Content-Type'];
|
||
} else if (res.header && res.header['content-type']) {
|
||
audioType = res.header['content-type'];
|
||
}
|
||
|
||
// 将arraybuffer转换为base64或blob URL
|
||
// #ifdef H5
|
||
// H5平台:使用Blob URL
|
||
const blob = new Blob([res.data], { type: audioType });
|
||
const audioUrl = URL.createObjectURL(blob);
|
||
// #endif
|
||
|
||
// #ifndef H5
|
||
// 非H5平台:使用base64
|
||
const base64 = uni.arrayBufferToBase64(res.data);
|
||
const audioUrl = `data:${audioType};base64,${base64}`;
|
||
// #endif
|
||
|
||
// 保存audioUrl到组件实例,以便在回调中清理
|
||
this.currentAudioUrl = audioUrl;
|
||
|
||
// 创建音频播放器实例
|
||
const innerAudioContext = uni.createInnerAudioContext();
|
||
innerAudioContext.src = audioUrl;
|
||
|
||
// 监听播放事件
|
||
innerAudioContext.onPlay(() => {
|
||
console.log('开始播放音频');
|
||
uni.hideLoading();
|
||
this.playingAudioId = audioId;
|
||
uni.showToast({
|
||
title: '开始播放',
|
||
icon: 'success',
|
||
duration: 1000
|
||
});
|
||
});
|
||
|
||
// 监听播放错误
|
||
innerAudioContext.onError((error) => {
|
||
console.error('音频播放错误:', error);
|
||
uni.hideLoading();
|
||
this.playingAudioId = null;
|
||
innerAudioContext.destroy();
|
||
this.innerAudioContext = null;
|
||
// #ifdef H5
|
||
// 清理Blob URL,释放内存
|
||
if (this.currentAudioUrl && this.currentAudioUrl.startsWith('blob:')) {
|
||
URL.revokeObjectURL(this.currentAudioUrl);
|
||
}
|
||
// #endif
|
||
this.currentAudioUrl = null;
|
||
uni.showToast({
|
||
title: '播放失败,请检查网络或音频文件',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
});
|
||
|
||
// 监听播放结束
|
||
innerAudioContext.onEnded(() => {
|
||
console.log('音频播放结束');
|
||
this.playingAudioId = null;
|
||
innerAudioContext.destroy();
|
||
this.innerAudioContext = null;
|
||
// #ifdef H5
|
||
// 清理Blob URL,释放内存
|
||
if (this.currentAudioUrl && this.currentAudioUrl.startsWith('blob:')) {
|
||
URL.revokeObjectURL(this.currentAudioUrl);
|
||
}
|
||
// #endif
|
||
this.currentAudioUrl = null;
|
||
});
|
||
|
||
// 监听播放暂停
|
||
innerAudioContext.onPause(() => {
|
||
console.log('音频播放暂停');
|
||
this.playingAudioId = null;
|
||
});
|
||
|
||
// 存储播放器实例
|
||
this.innerAudioContext = innerAudioContext;
|
||
|
||
// 开始播放
|
||
innerAudioContext.play();
|
||
|
||
} catch (error) {
|
||
uni.hideLoading();
|
||
console.error('播放录音失败:', error);
|
||
if (this.innerAudioContext) {
|
||
this.innerAudioContext.destroy();
|
||
this.innerAudioContext = null;
|
||
}
|
||
this.playingAudioId = null;
|
||
uni.showToast({
|
||
title: `播放失败: ${error.errMsg || error.message || '未知错误'}`,
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
},
|
||
// 停止播放录音
|
||
async stopAudioPlay(item) {
|
||
try {
|
||
// 停止音频播放器
|
||
if (this.innerAudioContext) {
|
||
this.innerAudioContext.stop();
|
||
this.innerAudioContext.destroy();
|
||
this.innerAudioContext = null;
|
||
}
|
||
|
||
// 清除播放状态
|
||
this.playingAudioId = null;
|
||
|
||
uni.showToast({
|
||
title: '已停止',
|
||
icon: 'success',
|
||
duration: 1000
|
||
});
|
||
} catch (error) {
|
||
// 即使出错,也清除播放状态
|
||
if (this.innerAudioContext) {
|
||
try {
|
||
this.innerAudioContext.stop();
|
||
this.innerAudioContext.destroy();
|
||
} catch (e) {
|
||
console.error('销毁音频播放器失败:', e);
|
||
}
|
||
this.innerAudioContext = null;
|
||
}
|
||
this.playingAudioId = null;
|
||
console.error('停止播放失败:', error);
|
||
uni.showToast({
|
||
title: '已停止',
|
||
icon: 'success',
|
||
duration: 1000
|
||
});
|
||
}
|
||
},
|
||
// 转文本
|
||
async transcribeAudio(item, index) {
|
||
try {
|
||
// 获取录音ID(可能是 id 或 segmentId)
|
||
const audioId = item.id || item.segmentId;
|
||
if (!audioId) {
|
||
uni.showToast({
|
||
title: '无法获取录音ID',
|
||
icon: 'none'
|
||
});
|
||
return;
|
||
}
|
||
|
||
// 如果正在转文本,则不再处理
|
||
if (this.transcribingIds.includes(audioId)) {
|
||
return;
|
||
}
|
||
|
||
// 添加到转文本列表
|
||
this.transcribingIds.push(audioId);
|
||
|
||
// 调用转文本接口:/api/audioManagementSegments/transcribe/{id}
|
||
const url = getApiUrl(`/api/audioManagementSegments/transcribe/${audioId}`);
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
const res = await uni.request({
|
||
url: url,
|
||
method: 'POST',
|
||
header: headers,
|
||
timeout: 30000 // 转文本可能需要较长时间
|
||
});
|
||
|
||
// 从转文本列表中移除
|
||
const idIndex = this.transcribingIds.indexOf(audioId);
|
||
if (idIndex > -1) {
|
||
this.transcribingIds.splice(idIndex, 1);
|
||
}
|
||
|
||
if (res.statusCode === 200 && res.data && res.data.success) {
|
||
uni.showToast({
|
||
title: res.data.message || '转文本请求已提交',
|
||
icon: 'success',
|
||
duration: 2000
|
||
});
|
||
|
||
// 如果返回了更新后的数据,更新列表中的该项
|
||
if (res.data.data) {
|
||
// 更新列表中的该项
|
||
this.$set(this.audioSegmentsList, index, {
|
||
...item,
|
||
...res.data.data
|
||
});
|
||
} else {
|
||
// 如果没有返回数据,可以刷新整个列表
|
||
// 或者等待用户手动刷新
|
||
// 这里先不自动刷新,因为转文本是后台处理
|
||
}
|
||
} else {
|
||
uni.showToast({
|
||
title: res.data?.message || '转文本请求失败',
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
} catch (error) {
|
||
// 从转文本列表中移除
|
||
const audioId = item.id || item.segmentId;
|
||
const idIndex = this.transcribingIds.indexOf(audioId);
|
||
if (idIndex > -1) {
|
||
this.transcribingIds.splice(idIndex, 1);
|
||
}
|
||
|
||
console.error('转文本失败:', error);
|
||
uni.showToast({
|
||
title: `转文本失败: ${error.errMsg || error.message || '未知错误'}`,
|
||
icon: 'none',
|
||
duration: 2000
|
||
});
|
||
}
|
||
},
|
||
// 格式化时长(秒转换为时分秒)
|
||
formatDuration(seconds) {
|
||
if (!seconds) return '00:00:00';
|
||
const hours = Math.floor(seconds / 3600);
|
||
const minutes = Math.floor((seconds % 3600) / 60);
|
||
const secs = seconds % 60;
|
||
return `${String(hours).padStart(2, '0')}:${String(minutes).padStart(2, '0')}:${String(secs).padStart(2, '0')}`;
|
||
},
|
||
// 格式化日期时间
|
||
formatDateTime(dateTime) {
|
||
if (!dateTime) return '';
|
||
const date = new Date(dateTime);
|
||
const year = date.getFullYear();
|
||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||
const day = String(date.getDate()).padStart(2, '0');
|
||
const hours = String(date.getHours()).padStart(2, '0');
|
||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
|
||
},
|
||
// 格式化录音文件名:只显示第一个中划线之后和最后一个中划线之前的内容
|
||
formatAudioFileName(fileName) {
|
||
if (!fileName) return '未命名录音';
|
||
// 找到第一个中划线的位置
|
||
const firstDashIndex = fileName.indexOf('-');
|
||
if (firstDashIndex === -1) {
|
||
// 如果没有中划线,直接返回原文件名
|
||
return fileName;
|
||
}
|
||
// 找到最后一个中划线的位置
|
||
const lastDashIndex = fileName.lastIndexOf('-');
|
||
if (firstDashIndex === lastDashIndex) {
|
||
// 如果只有一个中划线,返回中划线之后的内容
|
||
return fileName.substring(firstDashIndex + 1);
|
||
}
|
||
// 返回第一个中划线之后到最后一个中划线之前的内容
|
||
return fileName.substring(firstDashIndex + 1, lastDashIndex);
|
||
},
|
||
// 截断文本,只显示指定长度
|
||
truncateText(text, maxLength) {
|
||
if (!text) return '';
|
||
if (text.length <= maxLength) return text;
|
||
return text.substring(0, maxLength) + '...';
|
||
},
|
||
closeDetail() {
|
||
this.$refs.serviceDetailPopup.close();
|
||
},
|
||
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 {
|
||
// 构建查询参数对象(后端使用 @RequestParam,参数需要作为 URL 查询参数传递)
|
||
const queryParams = {
|
||
current: this.serviceListPage.current,
|
||
size: this.serviceListPage.size,
|
||
serviceStatus: '服务结束' // 默认搜索条件:服务结束
|
||
};
|
||
|
||
// 添加查询参数:所属销售姓名和客户姓名
|
||
const trimmedSalesName = params.salesName?.trim();
|
||
const trimmedCustomerName = params.customerName?.trim();
|
||
if (trimmedSalesName) {
|
||
queryParams.salesName = trimmedSalesName;
|
||
}
|
||
if (trimmedCustomerName) {
|
||
queryParams.customerName = trimmedCustomerName;
|
||
}
|
||
|
||
// 将参数转换为 URL 查询字符串(因为后端使用 @RequestParam)
|
||
const queryString = Object.keys(queryParams)
|
||
.filter(key => queryParams[key] !== null && queryParams[key] !== undefined && queryParams[key] !== '')
|
||
.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(queryParams[key])}`)
|
||
.join('&');
|
||
|
||
// 调试:打印请求参数
|
||
console.log('服务记录查询参数:', JSON.stringify(queryParams));
|
||
console.log('查询字符串:', queryString);
|
||
|
||
// 后端使用 @RequestParam,参数需要作为 URL 查询参数传递
|
||
const baseUrl = getApiUrl('/api/audioManagement/list');
|
||
const url = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
||
|
||
// 获取认证信息
|
||
let tenantId = '';
|
||
let token = '';
|
||
try {
|
||
tenantId = uni.getStorageSync('backend-tenant-id') || '';
|
||
token = uni.getStorageSync('backend-token') || '';
|
||
} catch (e) {
|
||
console.error('获取认证信息失败:', e);
|
||
}
|
||
|
||
// 构建请求头(参考接待页面的实现方式)
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
if (token) {
|
||
headers['Authorization'] = `Bearer ${token}`;
|
||
}
|
||
if (tenantId) {
|
||
headers['X-Tenant-Id'] = tenantId;
|
||
}
|
||
|
||
const res = await uni.request({
|
||
url,
|
||
method: 'POST',
|
||
data: {}, // POST 请求体为空,参数都在 URL 查询字符串中
|
||
header: headers,
|
||
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}`;
|
||
}
|
||
|
||
// 处理录音时长:确保转换为数字,如果是 null/undefined 则保留为 null
|
||
let durationValue = null;
|
||
if (item.duration !== null && item.duration !== undefined && item.duration !== '') {
|
||
const numDuration = Number(item.duration);
|
||
durationValue = isNaN(numDuration) ? null : numDuration;
|
||
}
|
||
|
||
return {
|
||
id: item.id,
|
||
title: `${item.salesName || ''}的服务记录`,
|
||
date: formattedTime ? `时间:${formattedTime}` : '',
|
||
description: item.summary || '',
|
||
customerName: item.customerName || '',
|
||
recordingName: item.recordingName || '', // 记录名称
|
||
duration: durationValue, // 录音时长(分钟),数字类型,null 表示无数据
|
||
// 保留原始数据,用于详情页
|
||
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: 0; /* 移除padding-top,因为tab页和列表组件都使用绝对定位 */
|
||
margin-top: 0;
|
||
position: relative;
|
||
min-height: 100vh; /* 确保content有足够高度 */
|
||
}
|
||
|
||
/* 隐藏导航栏占位区域,避免产生空白 */
|
||
::v-deep .uni-navbar__placeholder {
|
||
display: none !important;
|
||
}
|
||
|
||
/* 标签页 - 使用绝对定位紧贴导航栏 */
|
||
.tabs {
|
||
display: flex;
|
||
background-color: #FFFFFF;
|
||
border-bottom: 1px solid #E0E0E0;
|
||
padding: 0 32rpx;
|
||
position: absolute;
|
||
top: 88rpx; /* 导航栏高度 */
|
||
left: 0;
|
||
right: 0;
|
||
z-index: 1;
|
||
}
|
||
|
||
.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;
|
||
}
|
||
|
||
/* 录音列表弹窗样式 */
|
||
.audio-list-modal {
|
||
background-color: #FFFFFF;
|
||
border-radius: 32rpx 32rpx 0 0;
|
||
max-height: 90vh;
|
||
height: 90vh;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.audio-list-content {
|
||
flex: 1;
|
||
padding: 24rpx 32rpx;
|
||
box-sizing: border-box;
|
||
background-color: #F5F5F5;
|
||
min-height: 0; /* 配合 flex: 1 使用,确保可以滚动 */
|
||
height: 100%;
|
||
}
|
||
|
||
.audio-list-item {
|
||
background-color: #FFFFFF;
|
||
border-radius: 16rpx;
|
||
padding: 32rpx;
|
||
margin-bottom: 24rpx;
|
||
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
}
|
||
|
||
.audio-item-header {
|
||
margin-bottom: 16rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.audio-play-btn {
|
||
min-width: 80rpx;
|
||
height: 56rpx;
|
||
padding: 0 20rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
margin-right: 16rpx;
|
||
cursor: pointer;
|
||
border-radius: 8rpx;
|
||
transition: background-color 0.2s;
|
||
flex-shrink: 0;
|
||
background-color: #F5F5F5;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.audio-play-btn:active {
|
||
background-color: #E0E0E0;
|
||
}
|
||
|
||
.audio-play-text {
|
||
font-size: 24rpx;
|
||
color: #2A68FF;
|
||
font-weight: 500;
|
||
line-height: 1;
|
||
}
|
||
|
||
.audio-play-text--playing {
|
||
color: #FF3B30;
|
||
}
|
||
|
||
.audio-item-name {
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #333;
|
||
flex: 1;
|
||
}
|
||
|
||
.audio-item-info {
|
||
margin-bottom: 16rpx;
|
||
}
|
||
|
||
.audio-item-text {
|
||
font-size: 28rpx;
|
||
color: #666;
|
||
line-height: 1.6;
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
display: block;
|
||
}
|
||
|
||
.audio-item-text--empty {
|
||
color: #999;
|
||
font-style: italic;
|
||
}
|
||
|
||
.audio-item-transcribe {
|
||
display: flex;
|
||
align-items: center;
|
||
}
|
||
|
||
.transcribe-btn {
|
||
padding: 12rpx 24rpx;
|
||
background-color: #007AFF;
|
||
border-radius: 8rpx;
|
||
display: inline-flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
transition: background-color 0.2s, opacity 0.2s;
|
||
}
|
||
|
||
.transcribe-btn:active {
|
||
background-color: #0056CC;
|
||
}
|
||
|
||
.transcribe-btn--loading {
|
||
opacity: 0.6;
|
||
background-color: #999;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.transcribe-btn text {
|
||
font-size: 26rpx;
|
||
color: #FFFFFF;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.audio-item-meta {
|
||
display: flex;
|
||
flex-wrap: nowrap;
|
||
gap: 16rpx;
|
||
padding-top: 16rpx;
|
||
border-top: 1px solid #F0F0F0;
|
||
align-items: center;
|
||
}
|
||
|
||
.audio-meta-item {
|
||
font-size: 24rpx;
|
||
color: #999;
|
||
white-space: nowrap;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.audio-list-empty {
|
||
padding: 48rpx 0;
|
||
text-align: center;
|
||
color: #999;
|
||
font-size: 28rpx;
|
||
}
|
||
|
||
/* 录音文本内容弹框样式 */
|
||
/* 提高文本内容弹框的层级,确保显示在录音列表弹框上方 */
|
||
/* 通过深度选择器覆盖uni-popup的z-index */
|
||
.text-modal-popup {
|
||
z-index: 1000 !important;
|
||
}
|
||
|
||
.text-modal-popup .uni-popup {
|
||
z-index: 1000 !important;
|
||
}
|
||
|
||
.text-modal {
|
||
width: 100%;
|
||
height: 80vh;
|
||
max-height: 80vh;
|
||
background-color: #FFFFFF;
|
||
border-radius: 24rpx;
|
||
display: flex;
|
||
flex-direction: column;
|
||
overflow: hidden;
|
||
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.12);
|
||
position: relative;
|
||
}
|
||
|
||
.text-modal-header {
|
||
padding: 32rpx;
|
||
border-bottom: 1px solid #F0F0F0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.text-modal-title {
|
||
font-size: 32rpx;
|
||
font-weight: 600;
|
||
color: #333;
|
||
}
|
||
|
||
.text-modal-close {
|
||
width: 48rpx;
|
||
height: 48rpx;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
cursor: pointer;
|
||
border-radius: 50%;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.text-modal-close:active {
|
||
background-color: #F5F5F5;
|
||
}
|
||
|
||
.text-modal-content {
|
||
flex: 1;
|
||
height: 0;
|
||
min-height: 0;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.text-modal-text {
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
line-height: 1.8;
|
||
white-space: pre-wrap;
|
||
word-wrap: break-word;
|
||
word-break: break-all;
|
||
display: block;
|
||
padding: 32rpx;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
</style>
|
||
|