Files
DJKB/my-vue-app/src/views/maneger/components/MemberDetails.vue
lbw_9527443 f93236ab36 feat: 初始化Vue3项目并添加核心功能模块
新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖
添加路由配置和用户认证状态管理
实现销售数据看板、客户画像、团队管理等核心功能模块
集成图表库和API请求工具,完成基础样式配置
2025-08-12 14:34:44 +08:00

902 lines
20 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="member-details">
<div class="details-header" @click="toggleDetailsCollapse">
<h2>{{ selectedMember.name }} 的详细数据</h2>
<div class="collapse-toggle" :class="{ 'collapsed': isDetailsCollapsed }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 4H4l4-4z"/>
</svg>
</div>
</div>
<div class="details-content">
<div class="details-grid" v-show="!isDetailsCollapsed" :class="{ 'collapsing': isDetailsCollapsed }">
<div class="detail-card">
<div class="detail-label">总通话次数</div>
<div class="detail-value">{{ selectedMember.calls }} </div>
</div>
<div class="detail-card">
<div class="detail-label">通话时长</div>
<div class="detail-value">{{ selectedMember.callTime }} 小时</div>
</div>
<div class="detail-card">
<div class="detail-label">新增客户</div>
<div class="detail-value">{{ selectedMember.newClients }} </div>
</div>
<div class="detail-card">
<div class="detail-label">成交单数</div>
<div class="detail-value">{{ selectedMember.deals }} </div>
</div>
<div class="detail-card">
<div class="detail-label">总业绩</div>
<div class="detail-value">¥{{ selectedMember.performance.toLocaleString() }}</div>
</div>
<div class="detail-card">
<div class="detail-label">平均单价</div>
<div class="detail-value">¥{{ selectedMember.avgDealValue.toLocaleString() }}</div>
</div>
</div>
</div>
<!-- 指导建议 -->
<div class="guidance-section">
<div class="guidance-header" @click="toggleGuidanceCollapse">
<h3>💡 指导建议</h3>
<div class="collapse-toggle" :class="{ 'collapsed': isGuidanceCollapsed }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 4H4l4-4z"/>
</svg>
</div>
</div>
<div class="guidance-cards" v-show="!isGuidanceCollapsed" :class="{ 'collapsing': isGuidanceCollapsed }">
<div class="guidance-card" v-if="getGuidanceForMember(selectedMember).length > 0">
<div class="guidance-item" v-for="(guidance, index) in getGuidanceForMember(selectedMember)" :key="index">
<div class="guidance-icon" :class="guidance.type">
{{ guidance.icon }}
</div>
<div class="guidance-content">
<h4 class="guidance-title">{{ guidance.title }}</h4>
<p class="guidance-description">{{ guidance.description }}</p>
<div class="guidance-action" v-if="guidance.action">
<span class="action-label">建议行动:</span>
<span class="action-text">{{ guidance.action }}</span>
</div>
</div>
</div>
</div>
<div class="no-guidance" v-else>
<div class="celebration-icon">🎉</div>
<h4>表现优秀</h4>
<p>{{ selectedMember.name }} 的各项指标都很不错继续保持这种状态</p>
</div>
</div>
</div>
<!-- 录音列表 -->
<div class="recordings-section">
<div class="recordings-header" @click="toggleRecordingsCollapse">
<h3>🎧 通话录音</h3>
<div class="collapse-toggle" :class="{ 'collapsed': isRecordingsCollapsed }">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 4l4 4H4l4-4z"/>
</svg>
</div>
</div>
<div class="recordings-content" v-show="!isRecordingsCollapsed" :class="{ 'collapsing': isRecordingsCollapsed }">
<div class="recordings-list" v-if="getRecordingsForMember(selectedMember).length > 0">
<div class="recording-item" v-for="(recording, index) in getRecordingsForMember(selectedMember)" :key="index">
<div class="recording-info">
<div class="recording-title">{{ recording.title }}</div>
<div class="recording-meta">
<span class="recording-date">{{ recording.date }}</span>
<span class="recording-duration">{{ recording.duration }}</span>
<span class="recording-type" :class="recording.type">{{ recording.typeLabel }}</span>
</div>
</div>
<button class="download-btn" @click="downloadRecording(recording)">
<svg width="16" height="16" viewBox="0 0 16 16" fill="currentColor">
<path d="M8 12l-4-4h3V4h2v4h3l-4 4z"/>
<path d="M2 14h12v1H2z"/>
</svg>
下载
</button>
</div>
</div>
<div class="no-recordings" v-else>
<div class="no-data-icon">📞</div>
<h4>暂无录音</h4>
<p>{{ selectedMember.name }} 还没有通话录音记录</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, defineProps, watch, nextTick } from 'vue'
// 定义props
const props = defineProps({
selectedMember: {
type: Object,
required: true
}
})
// 详细数据折叠状态
const isDetailsCollapsed = ref(false)
// 指导建议折叠状态(默认展开)
const isGuidanceCollapsed = ref(false)
// 录音列表折叠状态(默认展开)
const isRecordingsCollapsed = ref(false)
// 切换详细数据折叠状态
const toggleDetailsCollapse = () => {
isDetailsCollapsed.value = !isDetailsCollapsed.value
}
// 切换指导建议折叠状态
const toggleGuidanceCollapse = () => {
isGuidanceCollapsed.value = !isGuidanceCollapsed.value
}
// 切换录音列表折叠状态
const toggleRecordingsCollapse = () => {
isRecordingsCollapsed.value = !isRecordingsCollapsed.value
}
// 获取成员录音列表
const getRecordingsForMember = (member) => {
// 模拟录音数据实际项目中应该从API获取
const recordings = [
{
id: 1,
title: '客户咨询 - 张先生',
date: '2024-01-15 14:30',
duration: '12:35',
type: 'consultation',
typeLabel: '咨询',
fileUrl: '/recordings/test_recording.m4a' // 使用测试文件
},
{
id: 2,
title: '跟进回访 - 李女士',
date: '2024-01-15 10:15',
duration: '8:42',
type: 'followup',
typeLabel: '回访',
fileUrl: '/recordings/test_recording.m4a' // 使用测试文件
},
{
id: 3,
title: '成交确认 - 王总',
date: '2024-01-14 16:20',
duration: '15:18',
type: 'deal',
typeLabel: '成交',
fileUrl: '/recordings/test_recording.m4a' // 使用测试文件
}
]
// 根据成员ID返回对应的录音这里简化处理
return recordings.slice(0, Math.min(3, member.calls || 0))
}
// 下载录音文件
const downloadRecording = (recording) => {
// 创建下载链接
const link = document.createElement('a')
link.href = recording.fileUrl
link.download = `${recording.title}_${recording.date.replace(/[:\s]/g, '_')}.m4a`
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
// 实际项目中可能需要调用API来获取下载链接
console.log('下载录音:', recording.title)
}
// 获取成员指导建议
const getGuidanceForMember = (member) => {
const guidance = []
// 业绩相关建议
if (member.performance === 0) {
guidance.push({
type: 'urgent',
icon: '🚨',
title: '业绩突破',
description: '当前还未有成交记录,需要重点关注转化技巧和客户跟进。',
action: '建议参加销售技巧培训,加强客户需求挖掘'
})
} else if (member.performance < 80000) {
guidance.push({
type: 'warning',
icon: '📈',
title: '业绩提升',
description: '业绩有提升空间,可以通过优化沟通策略来提高转化率。',
action: '分析高业绩同事的沟通技巧,制定个人提升计划'
})
}
// 转化率相关建议
if (member.conversion < 3.0) {
guidance.push({
type: 'urgent',
icon: '🎯',
title: '转化率优化',
description: '转化率偏低,需要提升客户沟通和需求挖掘能力。',
action: '重点学习客户心理分析和异议处理技巧'
})
} else if (member.conversion < 6.0) {
guidance.push({
type: 'info',
icon: '💬',
title: '沟通技巧',
description: '转化率还有提升空间,建议优化沟通话术和客户关系维护。',
action: '观摩优秀同事的通话录音,学习有效沟通技巧'
})
}
// 通话相关建议
if (member.calls < 100) {
guidance.push({
type: 'warning',
icon: '📞',
title: '通话量提升',
description: '通话量偏少,增加客户接触频次有助于提升业绩。',
action: '制定每日通话计划,确保充足的客户接触量'
})
}
// 客户开发建议
if (member.newClients < 5) {
guidance.push({
type: 'info',
icon: '👥',
title: '客户开发',
description: '新客户开发数量较少,可以拓展更多潜在客户渠道。',
action: '利用社交媒体和转介绍扩大客户来源'
})
}
// 平均单价建议
if (member.avgDealValue > 0 && member.avgDealValue < 25000) {
guidance.push({
type: 'success',
icon: '💰',
title: '客单价提升',
description: '可以尝试推荐更高价值的课程套餐,提升平均客单价。',
action: '学习产品组合销售技巧,挖掘客户更深层次需求'
})
}
return guidance.slice(0, 3) // 最多显示3个建议
}
// 监听selectedMember变化重置滚动条位置
watch(() => props.selectedMember, () => {
nextTick(() => {
// 获取member-details容器元素并重置滚动位置
const memberDetailsEl = document.querySelector('.member-details')
if (memberDetailsEl) {
// 使用平滑滚动动画
memberDetailsEl.scrollTo({
top: 0,
behavior: 'smooth'
})
}
})
}, { immediate: false })
</script>
<style lang="scss" scoped>
// Member Details
.member-details {
background: white;
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
height: 600px; // 固定高度
overflow-y: auto; // 整个卡片可滚动
// 自定义滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-track {
background: #f1f5f9;
border-radius: 3px;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 3px;
&:hover {
background: #94a3b8;
}
}
h2 {
font-size: 1.2rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 1.5rem 0;
}
.details-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
flex: 1;
align-content: start;
}
.detail-card {
background: #f8fafc;
border-radius: 8px;
padding: 0.5rem;
text-align: center;
.detail-label {
color: #64748b;
font-size: 0.85rem;
margin-bottom: 0.5rem;
}
.detail-value {
color: #1e293b;
font-size: 1.1rem;
font-weight: 600;
}
}
}
// 详细数据折叠功能样式
.details-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 0.5rem 0;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 1rem;
transition: all 0.2s ease;
&:hover {
background-color: #f8fafc;
border-radius: 4px;
padding: 0.5rem;
margin: 0 -0.5rem 1rem -0.5rem;
}
h2 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: #1e293b;
}
}
.collapse-toggle {
display: flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
border-radius: 4px;
background-color: #f1f5f9;
color: #64748b;
transition: all 0.2s ease;
&:hover {
background-color: #e2e8f0;
color: #475569;
}
svg {
transition: transform 0.2s ease;
}
&.collapsed svg {
transform: rotate(180deg);
}
}
.details-grid {
transition: all 0.3s ease;
&.collapsing {
opacity: 0;
transform: translateY(-10px);
}
}
// 录音列表样式
.recordings-section {
margin-top: 1.5rem;
}
.recordings-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 0.5rem 0;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 1rem;
transition: all 0.2s ease;
&:hover {
background-color: #f8fafc;
border-radius: 4px;
padding: 0.5rem;
margin: 0 -0.5rem 1rem -0.5rem;
}
h3 {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
}
.recordings-content {
transition: all 0.3s ease;
&.collapsing {
opacity: 0;
transform: translateY(-10px);
}
}
.recordings-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.recording-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 8px;
transition: all 0.2s ease;
&:hover {
background: #f1f5f9;
border-color: #cbd5e1;
}
}
.recording-info {
flex: 1;
}
.recording-title {
font-size: 0.9rem;
font-weight: 600;
color: #1e293b;
margin-bottom: 0.5rem;
}
.recording-meta {
display: flex;
align-items: center;
gap: 1rem;
font-size: 0.8rem;
}
.recording-date {
color: #64748b;
}
.recording-duration {
color: #475569;
font-weight: 500;
}
.recording-type {
padding: 0.25rem 0.5rem;
border-radius: 4px;
font-size: 0.75rem;
font-weight: 500;
&.consultation {
background: #dbeafe;
color: #1e40af;
}
&.followup {
background: #fef3c7;
color: #92400e;
}
&.deal {
background: #d1fae5;
color: #065f46;
}
}
.download-btn {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
background: #3b82f6;
color: white;
border: none;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
&:hover {
background: #2563eb;
transform: translateY(-1px);
}
&:active {
transform: translateY(0);
}
svg {
flex-shrink: 0;
}
}
.no-recordings {
text-align: center;
padding: 2rem 1rem;
background: #f8fafc;
border-radius: 8px;
border: 1px solid #e2e8f0;
.no-data-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
h4 {
font-size: 1rem;
font-weight: 600;
color: #64748b;
margin: 0 0 0.5rem 0;
}
p {
font-size: 0.9rem;
color: #94a3b8;
margin: 0;
}
}
// 指导建议样式
.guidance-section {
margin-top: 1.5rem;
}
.guidance-header {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 0.5rem 0;
border-bottom: 1px solid #e2e8f0;
margin-bottom: 1rem;
transition: all 0.2s ease;
&:hover {
background-color: #f8fafc;
border-radius: 4px;
padding: 0.5rem;
margin: 0 -0.5rem 1rem -0.5rem;
}
h3 {
font-size: 1.1rem;
font-weight: 600;
color: #1e293b;
margin: 0;
display: flex;
align-items: center;
gap: 0.5rem;
}
}
.guidance-cards {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.guidance-card {
background: white;
border-radius: 8px;
padding: 1rem;
border: 1px solid #e2e8f0;
}
.guidance-item {
display: flex;
gap: 0.75rem;
padding: 0.75rem 0;
&:not(:last-child) {
border-bottom: 1px solid #f1f5f9;
}
}
.guidance-icon {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1rem;
flex-shrink: 0;
&.urgent {
background: #fef2f2;
border: 1px solid #fecaca;
}
&.warning {
background: #fffbeb;
border: 1px solid #fed7aa;
}
&.info {
background: #eff6ff;
border: 1px solid #bfdbfe;
}
&.success {
background: #f0fdf4;
border: 1px solid #bbf7d0;
}
}
.guidance-content {
flex: 1;
}
.guidance-title {
font-size: 0.9rem;
font-weight: 600;
color: #1e293b;
margin: 0 0 0.25rem 0;
}
.guidance-description {
font-size: 0.8rem;
color: #64748b;
margin: 0 0 0.5rem 0;
line-height: 1.4;
}
.guidance-action {
display: flex;
flex-direction: column;
gap: 0.25rem;
.action-label {
font-size: 0.75rem;
font-weight: 500;
color: #3b82f6;
}
.action-text {
font-size: 0.8rem;
color: #1e293b;
background: #f8fafc;
padding: 0.5rem;
border-radius: 4px;
border-left: 3px solid #3b82f6;
}
}
.no-guidance {
text-align: center;
padding: 2rem 1rem;
background: white;
border-radius: 8px;
border: 1px solid #e2e8f0;
.celebration-icon {
font-size: 2rem;
margin-bottom: 0.5rem;
}
h4 {
font-size: 1rem;
font-weight: 600;
color: #059669;
margin: 0 0 0.5rem 0;
}
p {
font-size: 0.9rem;
color: #64748b;
margin: 0;
}
}
// 移动端适配
@media (max-width: 768px) {
.member-details {
padding: 1rem;
height: auto;
max-height: 500px;
h2 {
font-size: 1.1rem;
margin-bottom: 1rem;
}
.details-grid {
grid-template-columns: repeat(2, 1fr);
gap: 0.75rem;
}
.detail-card {
padding: 0.75rem;
.detail-label {
font-size: 0.8rem;
}
.detail-value {
font-size: 1rem;
}
}
}
// 录音列表适配
.recordings-section {
margin-top: 1rem;
.recordings-header {
h3 {
font-size: 1rem;
}
}
.recordings-list {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 0.5rem;
}
.recording-item {
flex-direction: column;
align-items: stretch;
gap: 0.5rem;
padding: 0.75rem;
}
.recording-info {
.recording-title {
font-size: 0.8rem;
margin-bottom: 0.25rem;
line-height: 1.3;
}
.recording-meta {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
.recording-date {
font-size: 0.7rem;
}
.recording-duration {
font-size: 0.7rem;
}
.recording-type {
align-self: flex-start;
font-size: 0.65rem;
padding: 0.2rem 0.4rem;
}
}
}
.download-btn {
align-self: center;
padding: 0.5rem 0.8rem;
font-size: 0.75rem;
svg {
width: 12px;
height: 12px;
}
}
}
// 指导建议适配
.guidance-section {
margin-top: 1rem;
.guidance-header {
h3 {
font-size: 1rem;
}
}
.guidance-item {
flex-direction: column;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem 0;
.guidance-icon {
width: 28px;
height: 28px;
font-size: 0.9rem;
}
}
.guidance-title {
font-size: 0.85rem;
}
.guidance-description {
font-size: 0.75rem;
}
.guidance-action {
.action-label {
font-size: 0.7rem;
}
.action-text {
font-size: 0.75rem;
padding: 0.4rem;
}
}
}
}
@media (max-width: 480px) {
.member-details {
padding: 0.75rem;
border-radius: 8px;
}
.recording-item {
padding: 0.6rem;
.recording-meta {
gap: 0.25rem;
font-size: 0.75rem;
}
.download-btn {
padding: 0.5rem 1rem;
font-size: 0.75rem;
}
}
.guidance-item {
.guidance-icon {
width: 24px;
height: 24px;
font-size: 0.8rem;
}
}
}
</style>