feat: 初始化Vue3项目并添加核心功能模块
新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖 添加路由配置和用户认证状态管理 实现销售数据看板、客户画像、团队管理等核心功能模块 集成图表库和API请求工具,完成基础样式配置
This commit is contained in:
599
my-vue-app/src/views/person/components/RawDataCards.vue
Normal file
599
my-vue-app/src/views/person/components/RawDataCards.vue
Normal file
@@ -0,0 +1,599 @@
|
||||
<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 chatMessages" :key="index" class="message-item">
|
||||
<div class="message-header">
|
||||
<span class="message-sender">{{ message.sender }}</span>
|
||||
<span class="message-time">{{ message.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">共 {{ callData.count }} 次通话</span>
|
||||
<span class="content-time">总时长: {{ callData.totalDuration }}</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.type }}</span>
|
||||
<span class="call-duration">{{ call.duration }}</span>
|
||||
<span class="call-time">{{ call.time }}</span>
|
||||
</div>
|
||||
<div class="call-summary">{{ call.summary }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
// Props
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
default: () => ({})
|
||||
}
|
||||
})
|
||||
|
||||
// 当前激活的tab
|
||||
const activeTab = ref('chat')
|
||||
|
||||
// 表单字段数据
|
||||
const formFields = computed(() => {
|
||||
const contact = props.selectedContact
|
||||
if (!contact || !contact.details) {
|
||||
return [
|
||||
{ label: '姓名', value: '暂无数据' },
|
||||
{ label: '联系方式', value: '暂无数据' },
|
||||
{ label: '意向课程', value: '暂无数据' },
|
||||
{ label: '预算范围', value: '暂无数据' }
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
{ label: '客户姓名', value: contact.name || '暂无' },
|
||||
{ label: '孩子姓名', value: contact.details.childName || '暂无' },
|
||||
{ label: '孩子年龄', value: contact.details.childAge ? `${contact.details.childAge}岁` : '暂无' },
|
||||
{ label: '关注问题', value: contact.details.concerns?.join('、') || '暂无' },
|
||||
{ label: '预算范围', value: contact.details.budget || '暂无' },
|
||||
{ label: '偏好时间', value: contact.details.preferredTime || '暂无' },
|
||||
{ label: '销售阶段', value: contact.salesStage || '暂无' },
|
||||
{ label: '健康度', value: contact.health ? `${contact.health}%` : '暂无' }
|
||||
]
|
||||
})
|
||||
|
||||
// 聊天数据
|
||||
const chatData = computed(() => ({
|
||||
count: props.selectedContact?.chatCount || 127,
|
||||
lastMessage: props.selectedContact?.lastMessage || '1小时前'
|
||||
}))
|
||||
|
||||
// 通话数据
|
||||
const callData = computed(() => ({
|
||||
count: props.selectedContact?.callCount || 5,
|
||||
totalDuration: props.selectedContact?.totalCallDuration || '45分钟'
|
||||
}))
|
||||
|
||||
// 聊天消息列表
|
||||
const chatMessages = computed(() => {
|
||||
return [
|
||||
{
|
||||
sender: '客户',
|
||||
time: '今天 14:30',
|
||||
content: '你好,我想了解一下数学课程的具体安排和费用情况。'
|
||||
},
|
||||
{
|
||||
sender: '我',
|
||||
time: '今天 14:32',
|
||||
content: '您好!我们的数学课程分为基础班和提高班,根据孩子的年龄和基础来安排。费用方面,基础班是6000元/期,提高班是8000元/期。'
|
||||
},
|
||||
{
|
||||
sender: '客户',
|
||||
time: '今天 14:35',
|
||||
content: '孩子现在8岁,数学基础一般,应该选择哪个班级比较合适?'
|
||||
},
|
||||
{
|
||||
sender: '我',
|
||||
time: '今天 14:37',
|
||||
content: '建议先从基础班开始,我们会有专业的测评来确定孩子的具体水平,然后制定个性化的学习方案。'
|
||||
},
|
||||
{
|
||||
sender: '客户',
|
||||
time: '今天 15:20',
|
||||
content: '好的,那什么时候可以安排试听课呢?'
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
// 通话记录列表
|
||||
const callRecords = computed(() => {
|
||||
return [
|
||||
{
|
||||
type: '呼出',
|
||||
duration: '12分钟',
|
||||
time: '今天 10:30',
|
||||
summary: '初次沟通,了解客户基本需求。客户对数学课程比较感兴趣,孩子8岁,希望提高数学成绩。约定发送详细资料。'
|
||||
},
|
||||
{
|
||||
type: '呼入',
|
||||
duration: '8分钟',
|
||||
time: '昨天 16:45',
|
||||
summary: '客户主动来电咨询价格和上课时间。解答了关于师资力量和教学方法的问题。客户表示需要和家人商量。'
|
||||
},
|
||||
{
|
||||
type: '呼出',
|
||||
duration: '15分钟',
|
||||
time: '3天前 14:20',
|
||||
summary: '跟进客户需求,详细介绍了课程体系和教学理念。客户对一对一辅导很感兴趣,但对价格有些犹豫。'
|
||||
},
|
||||
{
|
||||
type: '呼出',
|
||||
duration: '6分钟',
|
||||
time: '5天前 11:15',
|
||||
summary: '首次电话联系,简单介绍了公司和课程概况。客户表示有兴趣,约定后续详细沟通。'
|
||||
}
|
||||
]
|
||||
})
|
||||
</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: 400px;
|
||||
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-summary {
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.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>
|
||||
Reference in New Issue
Block a user