feat: 初始化Vue3项目并添加核心功能模块

新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖
添加路由配置和用户认证状态管理
实现销售数据看板、客户画像、团队管理等核心功能模块
集成图表库和API请求工具,完成基础样式配置
This commit is contained in:
2025-08-12 14:34:44 +08:00
commit f93236ab36
71 changed files with 32821 additions and 0 deletions

View 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>