Files
DJKB/my-vue-app/src/views/person/components/RawDataCards.vue
lbw_9527443 1d63829ed6 fix(CustomerDetail): 移除SOP分析按钮的加载状态限制并更新API地址
refactor(RawDataCards): 重构通话记录卡片布局并添加时间格式化功能

- 在CustomerDetail组件中,简化SOP分析按钮的状态逻辑并更新API地址
- 在RawDataCards组件中,重新设计操作按钮布局,添加时间显示功能并优化样式
2025-09-12 17:19:41 +08:00

755 lines
19 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="raw-data-cards">
<div class="cards-container">
<!-- 表单信息卡片 -->
<div class="data-card form-card">
<div class="card-header">
<div class="card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="14,2 14,8 20,8" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16" y1="13" x2="8" y2="13" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<line x1="16" y1="17" x2="8" y2="17" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
<polyline points="10,9 9,9 8,9" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="card-title">表单信息</h3>
</div>
<div class="card-content">
<div class="form-data-list">
<div v-for="(field, index) in formFields" :key="index" class="form-field">
<span class="field-label">{{ field.label }}:</span>
<span class="field-value">{{ field.value }}</span>
</div>
</div>
</div>
</div>
<!-- 聊天记录和通话录音卡片 -->
<div class="data-card communication-card">
<div class="card-header">
<div class="card-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>
<h3 class="card-title">沟通记录</h3>
</div>
<div class="card-content">
<!-- Tab 切换 -->
<div class="tab-container">
<div class="tab-buttons">
<button
class="tab-btn"
:class="{ active: activeTab === 'chat' }"
@click="activeTab = 'chat'"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
聊天记录
</button>
<button
class="tab-btn"
:class="{ active: activeTab === 'call' }"
@click="activeTab = 'call'"
>
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72 12.84 12.84 0 0 0 .7 2.81 2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45 12.84 12.84 0 0 0 2.81.7A2 2 0 0 1 22 16.92z" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
通话录音
</button>
</div>
<!-- Tab 内容 -->
<div class="tab-content">
<!-- 聊天记录内容 -->
<div v-if="activeTab === 'chat'" class="chat-content">
<div class="content-header">
<span class="content-count"> {{ chatData.count }} 条消息</span>
<span class="content-time">最新: {{ chatData.lastMessage }}</span>
</div>
<div class="message-list">
<div v-for="(message, index) in props.chatInfo.messages" :key="index" class="message-item">
<div class="message-header">
<span class="message-sender">{{ message.format_direction }}</span>
<span class="message-time">{{ message.format_add_time }}</span>
</div>
<div class="message-text">{{ message.content }}</div>
</div>
</div>
</div>
<!-- 通话录音内容 -->
<div v-if="activeTab === 'call'" class="call-content">
<div class="content-header">
<span class="content-count"> {{ callRecords.length }} 次通话</span>
<span class="content-time">通话记录</span>
</div>
<div class="call-list">
<div v-for="(call, index) in callRecords" :key="index" class="call-item">
<div class="call-header">
<span class="call-type">用户: {{ call.user_name }}</span>
<span class="call-duration">客户: {{ call.customer_name }}</span>
</div>
<div class="call-actions">
<div class="action-buttons">
<button class="action-btn download-btn" @click="downloadRecording(call)">
<i class="icon-download"></i>
录音下载
</button>
<button class="action-btn view-btn" @click="viewTranscript(call)">
<i class="icon-view"></i>
查看原文
</button>
</div>
<div class="call-time-info">
<span class="call-duration">{{ formatDateTime(call.record_create_time) }}</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
import axios from 'axios'
// Props
const props = defineProps({
selectedContact: {
type: Object,
default: () => ({})
},
formInfo: {
type: Object,
default: () => ({})
},
chatInfo: {
type: Object,
default: () => ({})
},
callInfo: {
type: Object,
default: () => ({})
}
})
// Emits
const emit = defineEmits(['analyze-sop'])
// 当前激活的tab
const activeTab = ref('chat')
// 聊天消息列表
const chatMessages = computed(() => {
return props.chatInfo?.messages || []
})
// 表单字段数据
const formFields = computed(() => {
const formData = props.formInfo
if (!formData || Object.keys(formData).length === 0) {
return [
{ label: '姓名', value: '暂无数据' },
{ label: '联系方式', value: '暂无数据' },
{ label: '孩子信息', value: '暂无数据' },
{ label: '地区', value: '暂无数据' }
]
}
let fields = []
// 检查是否为第一种格式包含name, mobile等字段
if (formData.name || formData.mobile || formData.child_name) {
const customerInfo = [formData.name, formData.mobile, formData.child_relation, formData.occupation].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.child_name, formData.child_gender, formData.child_education].filter(item => item && item !== '暂无').join(' | ')
fields = [
{ label: '客户信息', value: customerInfo || '暂无' },
{ label: '孩子信息', value: childInfo || '暂无' },
{ label: '地区', value: formData.territory || '暂无' }
]
// 如果有additional_info添加所有问题
if (formData.additional_info && Array.isArray(formData.additional_info)) {
formData.additional_info.forEach((item) => {
fields.push({
label: item.topic,
value: item.answer
})
})
}
} else {
// 第二种格式expandXXX字段
const customerInfo = [formData.expandTwentyOne, formData.expandOne].filter(item => item && item !== '暂无').join(' | ')
const childInfo = [formData.expandTwentyNine, formData.expandTwentyFive, formData.expandTwo].filter(item => item && item !== '暂无').join(' | ')
fields = [
{ label: '客户信息', value: customerInfo || '暂无' },
{ label: '孩子信息', value: childInfo || '暂无' },
{ label: '学习状态', value: formData.expandFive || '暂无' },
{ label: '沟通情况', value: formData.expandEight || '暂无' },
{ label: '主要问题', value: formData.expandTwentySeven || '暂无' },
{ label: '关注领域', value: formData.expandFifteen || '暂无' },
{ label: '学习成绩', value: formData.expandFourteen || '暂无' },
{ label: '孩子数量', value: formData.expandTwenty || '暂无' },
{ label: '预期时间', value: formData.expandThirty || '暂无' }
]
}
// 合并表单数据和聊天数据
const allFields = [...fields]
return allFields
})
// 聊天数据
const chatData = computed(() => ({
count: props.chatInfo?.messages?.length || 0,
lastMessage: props.chatInfo?.update_time || '1小时前'
}))
// 通话数据
const callData = computed(() => ({
count: props.selectedContact?.callCount || 5,
totalDuration: props.selectedContact?.totalCallDuration || '45分钟'
}))
// 通话记录列表
const callRecords = computed(() => {
// 从 props.callInfo 中获取真实的通话记录数据
if (props.callInfo && Array.isArray(props.callInfo)) {
return props.callInfo
}
// 如果 callInfo 是单个对象API返回的数据格式
if (props.callInfo && typeof props.callInfo === 'object' && props.callInfo.user_name) {
return [props.callInfo] // 将单个对象包装成数组
}
// 如果 callInfo 是对象且包含数据数组
if (props.callInfo && props.callInfo && Array.isArray(props.callInfo)) {
return props.callInfo
}
// 如果没有数据,返回空数组
return []
})
// 录音下载方法
const downloadRecording = (call) => {
console.log('下载录音:', call)
// 检查是否有录音文件地址
if (call.record_file_addr) {
const recordingUrl = call.record_file_addr
// 从URL中提取文件名
const urlParts = recordingUrl.split('/')
const fileName = urlParts[urlParts.length - 1]
// 创建下载链接
const link = document.createElement('a')
link.href = recordingUrl
link.download = fileName
link.target = '_blank'
// 触发下载
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
} else {
alert('该通话记录暂无录音文件')
}
}
// 查看原文方法
const viewTranscript = async (call) => {
// 触发SOP分析
alert(call.record_context)
// 显示通话记录内容
if (call.record_context) {
alert(call.record_context)
} else {
alert('该通话记录暂无原文内容')
}
}
// 时间格式化方法
const formatDateTime = (dateTimeString) => {
if (!dateTimeString) return '暂无时间'
try {
const date = new Date(dateTimeString)
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}`
} catch (error) {
console.error('时间格式化错误:', error)
return dateTimeString
}
}
</script>
<style lang="scss" scoped>
.raw-data-cards {
margin: 24px 0;
}
.cards-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 20px;
@media (max-width: 768px) {
grid-template-columns: 1fr;
gap: 16px;
}
}
.data-card {
background: #ffffff;
border: 1px solid #e5e7eb;
border-radius: 12px;
padding: 20px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
border-color: #d1d5db;
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, #3b82f6, #8b5cf6);
}
&.form-card::before {
background: linear-gradient(90deg, #10b981, #059669);
}
&.communication-card::before {
background: linear-gradient(90deg, #3b82f6, #1d4ed8);
}
}
.card-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 16px;
}
.card-icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
background: #f3f4f6;
color: #6b7280;
.form-card & {
background: #ecfdf5;
color: #059669;
}
.communication-card & {
background: #eff6ff;
color: #1d4ed8;
}
}
.card-title {
font-size: 16px;
font-weight: 600;
color: #111827;
margin: 0;
flex: 1;
}
// 表单字段样式
.form-data-list {
.form-field {
display: flex;
justify-content: space-between;
align-items: center;
padding: 12px 0;
border-bottom: 1px solid #f3f4f6;
&:last-child {
border-bottom: none;
}
.field-label {
font-size: 14px;
color: #6b7280;
font-weight: 500;
}
.field-value {
font-size: 14px;
color: #1f2937;
font-weight: 500;
}
}
}
// Tab 容器样式
.tab-container {
.tab-buttons {
display: flex;
border-bottom: 1px solid #e5e7eb;
margin-bottom: 16px;
.tab-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 16px;
background: none;
border: none;
font-size: 14px;
font-weight: 500;
color: #6b7280;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 0.2s ease;
&.active {
color: #3b82f6;
border-bottom-color: #3b82f6;
}
&:hover {
color: #3b82f6;
}
svg {
width: 16px;
height: 16px;
}
}
}
.tab-content {
min-height: 300px;
max-height: 450px;
overflow-y: auto;
.content-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
padding-bottom: 12px;
border-bottom: 1px solid #f3f4f6;
.content-count {
font-size: 12px;
font-weight: 600;
color: #3b82f6;
}
.content-time {
font-size: 12px;
color: #9ca3af;
}
}
}
}
// 聊天消息样式
.message-list {
.message-item {
margin-bottom: 16px;
padding: 12px;
border-radius: 8px;
background: #f9fafb;
.message-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.message-sender {
font-size: 12px;
font-weight: 600;
color: #3b82f6;
}
.message-time {
font-size: 12px;
color: #9ca3af;
}
}
.message-text {
font-size: 14px;
color: #374151;
line-height: 1.5;
}
}
}
// 通话记录样式
.call-list {
.call-item {
margin-bottom: 16px;
padding: 16px;
border-radius: 8px;
background: #f9fafb;
border-left: 4px solid #3b82f6;
.call-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
.call-type {
font-size: 12px;
font-weight: 600;
padding: 4px 8px;
border-radius: 4px;
background: #dbeafe;
color: #3b82f6;
}
.call-duration {
font-size: 12px;
font-weight: 500;
color: #6b7280;
}
.call-time {
font-size: 12px;
color: #9ca3af;
}
}
.call-actions {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 12px;
padding-top: 8px;
border-top: 1px solid #f3f4f6;
.action-buttons {
display: flex;
gap: 8px;
}
.action-btn {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
border: none;
border-radius: 6px;
font-size: 11px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&.download-btn {
background: #dbeafe;
color: #3b82f6;
&:hover {
background: #bfdbfe;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
}
}
&.view-btn {
background: #d1fae5;
color: #059669;
&:hover {
background: #a7f3d0;
transform: translateY(-1px);
box-shadow: 0 2px 4px rgba(5, 150, 105, 0.2);
}
}
i {
width: 12px;
height: 12px;
&.icon-download::before {
content: '⬇';
}
&.icon-view::before {
content: '👁';
}
}
}
.call-time-info {
.call-duration {
font-size: 11px;
color: #6b7280;
font-weight: 500;
background: #f9fafb;
padding: 4px 8px;
border-radius: 4px;
border: 1px solid #e5e7eb;
}
}
}
}
}
.card-content {
margin-bottom: 20px;
}
.card-description {
color: #6b7280;
font-size: 14px;
margin: 0 0 16px 0;
line-height: 1.5;
}
.card-stats {
display: flex;
gap: 20px;
@media (max-width: 480px) {
flex-direction: column;
gap: 12px;
}
}
.stat-item {
display: flex;
flex-direction: column;
gap: 4px;
}
.stat-label {
font-size: 12px;
color: #9ca3af;
font-weight: 500;
}
.stat-value {
font-size: 14px;
color: #111827;
font-weight: 600;
}
.card-action {
border-top: 1px solid #f3f4f6;
padding-top: 16px;
margin-top: 16px;
}
.view-btn {
display: flex;
align-items: center;
gap: 8px;
background: none;
border: none;
color: #6b7280;
font-size: 14px;
font-weight: 500;
cursor: pointer;
padding: 8px 0;
transition: color 0.2s ease;
&:hover {
color: #111827;
}
svg {
transition: transform 0.2s ease;
}
&:hover svg {
transform: translateX(2px);
}
}
@media (max-width: 768px) {
.raw-data-cards {
margin: 20px 0;
}
.data-card {
padding: 16px;
}
.card-header {
gap: 10px;
margin-bottom: 14px;
}
.card-icon {
width: 36px;
height: 36px;
}
.card-title {
font-size: 15px;
}
}
@media (max-width: 480px) {
.cards-container {
gap: 12px;
}
.data-card {
padding: 14px;
}
.card-header {
gap: 8px;
margin-bottom: 12px;
}
.card-icon {
width: 32px;
height: 32px;
}
.card-title {
font-size: 14px;
}
.card-description {
font-size: 13px;
}
}
</style>