feat: 初始化Vue3项目并添加核心功能模块
新增项目基础结构,包括Vue3、Pinia、Element Plus等核心依赖 添加路由配置和用户认证状态管理 实现销售数据看板、客户画像、团队管理等核心功能模块 集成图表库和API请求工具,完成基础样式配置
This commit is contained in:
912
my-vue-app/src/views/secondTop/components/ActionItems.vue
Normal file
912
my-vue-app/src/views/secondTop/components/ActionItems.vue
Normal file
@@ -0,0 +1,912 @@
|
||||
<template>
|
||||
<div class="action-items">
|
||||
<div class="actions-header">
|
||||
<h2>待处理事项</h2>
|
||||
<div class="header-controls">
|
||||
<select v-model="filterPriority" class="priority-filter">
|
||||
<option value="all">全部优先级</option>
|
||||
<option value="urgent">紧急</option>
|
||||
<option value="high">高</option>
|
||||
<option value="medium">中</option>
|
||||
<option value="low">低</option>
|
||||
</select>
|
||||
<button class="add-btn" @click="showAddForm = true">+ 新增</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 统计概览 -->
|
||||
<div class="actions-summary">
|
||||
<div class="summary-item urgent">
|
||||
<div class="summary-count">{{ getCountByPriority('urgent') }}</div>
|
||||
<div class="summary-label">紧急事项</div>
|
||||
</div>
|
||||
<div class="summary-item high">
|
||||
<div class="summary-count">{{ getCountByPriority('high') }}</div>
|
||||
<div class="summary-label">高优先级</div>
|
||||
</div>
|
||||
<div class="summary-item medium">
|
||||
<div class="summary-count">{{ getCountByPriority('medium') }}</div>
|
||||
<div class="summary-label">中优先级</div>
|
||||
</div>
|
||||
<div class="summary-item completed">
|
||||
<div class="summary-count">{{ completedCount }}</div>
|
||||
<div class="summary-label">已完成</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 事项列表 -->
|
||||
<div class="actions-list">
|
||||
<div
|
||||
v-for="action in filteredActions"
|
||||
:key="action.id"
|
||||
class="action-item"
|
||||
:class="[action.priority, { completed: action.completed, overdue: isOverdue(action.dueDate) }]"
|
||||
>
|
||||
<div class="action-checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
:checked="action.completed"
|
||||
@change="toggleComplete(action.id)"
|
||||
class="checkbox"
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="action-content">
|
||||
<div class="action-header">
|
||||
<h4 class="action-title" :class="{ completed: action.completed }">{{ action.title }}</h4>
|
||||
<div class="action-meta">
|
||||
<span class="priority-badge" :class="action.priority">{{ getPriorityText(action.priority) }}</span>
|
||||
<span class="due-date" :class="{ overdue: isOverdue(action.dueDate) }">
|
||||
{{ formatDueDate(action.dueDate) }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p class="action-description">{{ action.description }}</p>
|
||||
|
||||
<div class="action-details">
|
||||
<div class="detail-item">
|
||||
<span class="detail-label">关联组别:</span>
|
||||
<span class="detail-value">{{ action.relatedGroup || '全部' }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="action.assignee">
|
||||
<span class="detail-label">负责人:</span>
|
||||
<span class="detail-value">{{ action.assignee }}</span>
|
||||
</div>
|
||||
<div class="detail-item" v-if="action.progress !== undefined">
|
||||
<span class="detail-label">进度:</span>
|
||||
<div class="progress-mini">
|
||||
<div class="progress-bar-mini">
|
||||
<div class="progress-fill-mini" :style="{ width: action.progress + '%' }"></div>
|
||||
</div>
|
||||
<span class="progress-text-mini">{{ action.progress }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-footer">
|
||||
<div class="action-tags">
|
||||
<span v-for="tag in action.tags" :key="tag" class="tag">{{ tag }}</span>
|
||||
</div>
|
||||
<div class="action-buttons">
|
||||
<button class="btn-edit" @click="editAction(action)">编辑</button>
|
||||
<button class="btn-delete" @click="deleteAction(action.id)">删除</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<div v-if="filteredActions.length === 0" class="empty-state">
|
||||
<div class="empty-icon">✅</div>
|
||||
<div class="empty-text">
|
||||
<h3>暂无待处理事项</h3>
|
||||
<p>{{ filterPriority === 'all' ? '所有事项都已处理完成' : '该优先级下暂无事项' }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增表单模态框 -->
|
||||
<div v-if="showAddForm" class="modal-overlay" @click="showAddForm = false">
|
||||
<div class="modal-content" @click.stop>
|
||||
<div class="modal-header">
|
||||
<h3>新增待处理事项</h3>
|
||||
<button class="close-btn" @click="showAddForm = false">×</button>
|
||||
</div>
|
||||
<form @submit.prevent="addAction" class="add-form">
|
||||
<div class="form-group">
|
||||
<label>标题</label>
|
||||
<input v-model="newAction.title" type="text" required class="form-input">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>描述</label>
|
||||
<textarea v-model="newAction.description" class="form-textarea"></textarea>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<div class="form-group">
|
||||
<label>优先级</label>
|
||||
<select v-model="newAction.priority" class="form-select">
|
||||
<option value="low">低</option>
|
||||
<option value="medium">中</option>
|
||||
<option value="high">高</option>
|
||||
<option value="urgent">紧急</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>截止日期</label>
|
||||
<input v-model="newAction.dueDate" type="date" class="form-input">
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>关联组别</label>
|
||||
<select v-model="newAction.relatedGroup" class="form-select">
|
||||
<option value="">全部</option>
|
||||
<option value="精英组">精英组</option>
|
||||
<option value="冲锋组">冲锋组</option>
|
||||
<option value="突破组">突破组</option>
|
||||
<option value="新星组">新星组</option>
|
||||
<option value="潜力组">潜力组</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-actions">
|
||||
<button type="button" @click="showAddForm = false" class="btn-cancel">取消</button>
|
||||
<button type="submit" class="btn-submit">添加</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
selectedGroup: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// 筛选优先级
|
||||
const filterPriority = ref('all')
|
||||
|
||||
// 显示新增表单
|
||||
const showAddForm = ref(false)
|
||||
|
||||
// 新增事项表单数据
|
||||
const newAction = ref({
|
||||
title: '',
|
||||
description: '',
|
||||
priority: 'medium',
|
||||
dueDate: '',
|
||||
relatedGroup: ''
|
||||
})
|
||||
|
||||
// 待处理事项数据
|
||||
const actions = ref([
|
||||
{
|
||||
id: 1,
|
||||
title: '突破组转化率改进计划',
|
||||
description: '针对突破组转化率连续下降问题,制定具体改进措施并跟踪执行',
|
||||
priority: 'urgent',
|
||||
dueDate: '2024-01-15',
|
||||
relatedGroup: '突破组',
|
||||
assignee: '王主管',
|
||||
progress: 30,
|
||||
tags: ['业绩改进', '紧急'],
|
||||
completed: false,
|
||||
createdAt: '2024-01-10'
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
title: '新星组人员补充',
|
||||
description: '新星组当前人员不足,需要招聘2名新销售并安排培训',
|
||||
priority: 'high',
|
||||
dueDate: '2024-01-20',
|
||||
relatedGroup: '新星组',
|
||||
assignee: '赵主管',
|
||||
progress: 60,
|
||||
tags: ['人员管理', '招聘'],
|
||||
completed: false,
|
||||
createdAt: '2024-01-08'
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
title: '月度业绩分析报告',
|
||||
description: '整理各组月度业绩数据,分析趋势并提出下月目标建议',
|
||||
priority: 'medium',
|
||||
dueDate: '2024-01-25',
|
||||
relatedGroup: '',
|
||||
assignee: '中心组长',
|
||||
progress: 80,
|
||||
tags: ['数据分析', '报告'],
|
||||
completed: false,
|
||||
createdAt: '2024-01-05'
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
title: '销售技能培训安排',
|
||||
description: '组织各组销售人员参加客户沟通技巧培训',
|
||||
priority: 'medium',
|
||||
dueDate: '2024-01-30',
|
||||
relatedGroup: '',
|
||||
assignee: '培训部',
|
||||
progress: 20,
|
||||
tags: ['培训', '技能提升'],
|
||||
completed: false,
|
||||
createdAt: '2024-01-12'
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
title: '客户满意度调研',
|
||||
description: '对已成交客户进行满意度调研,收集改进建议',
|
||||
priority: 'low',
|
||||
dueDate: '2024-02-05',
|
||||
relatedGroup: '',
|
||||
assignee: '客服部',
|
||||
progress: 0,
|
||||
tags: ['客户服务', '调研'],
|
||||
completed: false,
|
||||
createdAt: '2024-01-14'
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: '精英组激励方案制定',
|
||||
description: '为表现优秀的精英组制定专项激励方案',
|
||||
priority: 'medium',
|
||||
dueDate: '2024-01-18',
|
||||
relatedGroup: '精英组',
|
||||
assignee: '人事部',
|
||||
progress: 100,
|
||||
tags: ['激励', '团队管理'],
|
||||
completed: true,
|
||||
createdAt: '2024-01-01'
|
||||
}
|
||||
])
|
||||
|
||||
// 筛选后的事项
|
||||
const filteredActions = computed(() => {
|
||||
let filtered = actions.value
|
||||
|
||||
if (filterPriority.value !== 'all') {
|
||||
filtered = filtered.filter(action => action.priority === filterPriority.value)
|
||||
}
|
||||
|
||||
// 如果选中了特定组别,优先显示相关事项
|
||||
if (props.selectedGroup) {
|
||||
filtered = filtered.sort((a, b) => {
|
||||
const aRelated = a.relatedGroup === props.selectedGroup.name
|
||||
const bRelated = b.relatedGroup === props.selectedGroup.name
|
||||
if (aRelated && !bRelated) return -1
|
||||
if (!aRelated && bRelated) return 1
|
||||
return 0
|
||||
})
|
||||
}
|
||||
|
||||
return filtered.filter(action => !action.completed)
|
||||
})
|
||||
|
||||
// 已完成数量
|
||||
const completedCount = computed(() => {
|
||||
return actions.value.filter(action => action.completed).length
|
||||
})
|
||||
|
||||
// 按优先级获取数量
|
||||
const getCountByPriority = (priority) => {
|
||||
return actions.value.filter(action => action.priority === priority && !action.completed).length
|
||||
}
|
||||
|
||||
// 切换完成状态
|
||||
const toggleComplete = (id) => {
|
||||
const action = actions.value.find(a => a.id === id)
|
||||
if (action) {
|
||||
action.completed = !action.completed
|
||||
}
|
||||
}
|
||||
|
||||
// 判断是否过期
|
||||
const isOverdue = (dueDate) => {
|
||||
return new Date(dueDate) < new Date()
|
||||
}
|
||||
|
||||
// 获取优先级文本
|
||||
const getPriorityText = (priority) => {
|
||||
const priorityMap = {
|
||||
urgent: '紧急',
|
||||
high: '高',
|
||||
medium: '中',
|
||||
low: '低'
|
||||
}
|
||||
return priorityMap[priority] || priority
|
||||
}
|
||||
|
||||
// 格式化截止日期
|
||||
const formatDueDate = (dueDate) => {
|
||||
const date = new Date(dueDate)
|
||||
const today = new Date()
|
||||
const diffTime = date - today
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
|
||||
if (diffDays < 0) {
|
||||
return `逾期${Math.abs(diffDays)}天`
|
||||
} else if (diffDays === 0) {
|
||||
return '今天到期'
|
||||
} else if (diffDays === 1) {
|
||||
return '明天到期'
|
||||
} else {
|
||||
return `${diffDays}天后到期`
|
||||
}
|
||||
}
|
||||
|
||||
// 编辑事项
|
||||
const editAction = (action) => {
|
||||
// 这里可以实现编辑功能
|
||||
console.log('编辑事项:', action)
|
||||
}
|
||||
|
||||
// 删除事项
|
||||
const deleteAction = (id) => {
|
||||
if (confirm('确定要删除这个事项吗?')) {
|
||||
const index = actions.value.findIndex(a => a.id === id)
|
||||
if (index > -1) {
|
||||
actions.value.splice(index, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 添加新事项
|
||||
const addAction = () => {
|
||||
const newId = Math.max(...actions.value.map(a => a.id)) + 1
|
||||
actions.value.push({
|
||||
id: newId,
|
||||
...newAction.value,
|
||||
progress: 0,
|
||||
tags: [],
|
||||
completed: false,
|
||||
createdAt: new Date().toISOString().split('T')[0]
|
||||
})
|
||||
|
||||
// 重置表单
|
||||
newAction.value = {
|
||||
title: '',
|
||||
description: '',
|
||||
priority: 'medium',
|
||||
dueDate: '',
|
||||
relatedGroup: ''
|
||||
}
|
||||
|
||||
showAddForm.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.action-items {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.actions-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.header-controls {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
align-items: center;
|
||||
|
||||
.priority-filter {
|
||||
padding: 0.5rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
background: white;
|
||||
cursor: pointer;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
padding: 0.5rem 1rem;
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: background 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 统计概览
|
||||
.actions-summary {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
.summary-item {
|
||||
text-align: center;
|
||||
padding: 1rem;
|
||||
border-radius: 8px;
|
||||
|
||||
&.urgent {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.high {
|
||||
background: #fef3c7;
|
||||
border: 1px solid #fed7aa;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: #eff6ff;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
}
|
||||
|
||||
.summary-count {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.summary-label {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 事项列表
|
||||
.actions-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
|
||||
.action-item {
|
||||
display: flex;
|
||||
padding: 1rem;
|
||||
border: 1px solid #e5e7eb;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 1rem;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.urgent {
|
||||
border-left: 4px solid #ef4444;
|
||||
}
|
||||
|
||||
&.high {
|
||||
border-left: 4px solid #f59e0b;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
border-left: 4px solid #3b82f6;
|
||||
}
|
||||
|
||||
&.low {
|
||||
border-left: 4px solid #10b981;
|
||||
}
|
||||
|
||||
&.completed {
|
||||
opacity: 0.6;
|
||||
background: #f9fafb;
|
||||
}
|
||||
|
||||
&.overdue {
|
||||
background: #fef2f2;
|
||||
}
|
||||
|
||||
.action-checkbox {
|
||||
margin-right: 1rem;
|
||||
|
||||
.checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.action-content {
|
||||
flex: 1;
|
||||
|
||||
.action-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.action-title {
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin: 0;
|
||||
|
||||
&.completed {
|
||||
text-decoration: line-through;
|
||||
color: #9ca3af;
|
||||
}
|
||||
}
|
||||
|
||||
.action-meta {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
|
||||
.priority-badge {
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
|
||||
&.urgent {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
&.high {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
&.medium {
|
||||
background: #dbeafe;
|
||||
color: #1e40af;
|
||||
}
|
||||
|
||||
&.low {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
}
|
||||
|
||||
.due-date {
|
||||
font-size: 0.8rem;
|
||||
color: #6b7280;
|
||||
|
||||
&.overdue {
|
||||
color: #ef4444;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-description {
|
||||
font-size: 0.9rem;
|
||||
color: #6b7280;
|
||||
margin: 0 0 1rem 0;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.action-details {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.detail-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.detail-label {
|
||||
font-size: 0.8rem;
|
||||
color: #9ca3af;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.detail-value {
|
||||
font-size: 0.8rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.progress-mini {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
|
||||
.progress-bar-mini {
|
||||
width: 60px;
|
||||
height: 4px;
|
||||
background: #f3f4f6;
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
|
||||
.progress-fill-mini {
|
||||
height: 100%;
|
||||
background: #3b82f6;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.progress-text-mini {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-footer {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
.action-tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
.tag {
|
||||
padding: 0.25rem 0.5rem;
|
||||
background: #f3f4f6;
|
||||
color: #6b7280;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
.btn-edit, .btn-delete {
|
||||
padding: 0.25rem 0.75rem;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
font-size: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-edit {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
|
||||
&:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-delete {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
|
||||
&:hover {
|
||||
background: #fecaca;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 空状态
|
||||
.empty-state {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
|
||||
.empty-icon {
|
||||
font-size: 3rem;
|
||||
margin-bottom: 1rem;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.empty-text {
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
color: #374151;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #6b7280;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 模态框
|
||||
.modal-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
z-index: 1000;
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
width: 90%;
|
||||
max-width: 500px;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.close-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 1.5rem;
|
||||
cursor: pointer;
|
||||
color: #6b7280;
|
||||
|
||||
&:hover {
|
||||
color: #374151;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.add-form {
|
||||
.form-group {
|
||||
margin-bottom: 1rem;
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
color: #374151;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.form-input, .form-textarea, .form-select {
|
||||
width: 100%;
|
||||
padding: 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
height: 80px;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.form-actions {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
justify-content: flex-end;
|
||||
margin-top: 1.5rem;
|
||||
|
||||
.btn-cancel, .btn-submit {
|
||||
padding: 0.75rem 1.5rem;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.btn-cancel {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
|
||||
&:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
}
|
||||
|
||||
.btn-submit {
|
||||
background: #3b82f6;
|
||||
color: white;
|
||||
|
||||
&:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.action-items {
|
||||
padding: 1rem;
|
||||
|
||||
.actions-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.actions-summary {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.action-item {
|
||||
.action-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.action-details {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.action-footer {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
.form-row {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
246
my-vue-app/src/views/secondTop/components/CenterOverview.vue
Normal file
246
my-vue-app/src/views/secondTop/components/CenterOverview.vue
Normal file
@@ -0,0 +1,246 @@
|
||||
<template>
|
||||
<div class="center-overview">
|
||||
<h2>中心整体概览</h2>
|
||||
<div class="overview-grid">
|
||||
<div class="overview-card primary">
|
||||
<div class="card-header">
|
||||
<span class="card-title">中心总业绩</span>
|
||||
<span class="card-trend positive">+12% vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">552,000 元</div>
|
||||
<div class="card-subtitle">月目标完成率: 56%</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">活跃组数</span>
|
||||
<span class="card-trend stable">5/5 组</span>
|
||||
</div>
|
||||
<div class="card-value">5 组</div>
|
||||
<div class="card-subtitle">总人数: 40人</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">中心转化率</span>
|
||||
<span class="card-trend positive">+0.3% vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">5.2%</div>
|
||||
<div class="card-subtitle">行业平均: 4.8%</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">总通话次数</span>
|
||||
<span class="card-trend positive">+8% vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">1,247 次</div>
|
||||
<div class="card-subtitle">有效通话: 892次</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">新增客户</span>
|
||||
<span class="card-trend positive">+15% vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">117 人</div>
|
||||
<div class="card-subtitle">意向客户: 89人</div>
|
||||
</div>
|
||||
|
||||
<div class="overview-card">
|
||||
<div class="card-header">
|
||||
<span class="card-title">定金转化</span>
|
||||
<span class="card-trend positive">+18% vs 上期</span>
|
||||
</div>
|
||||
<div class="card-value">40 单</div>
|
||||
<div class="card-subtitle">平均定金转化率: 13,800元</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// 中心整体概览组件
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.center-overview {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 0.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
|
||||
h2 {
|
||||
font-size: 1.3rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 1.5rem 0;
|
||||
}
|
||||
|
||||
.overview-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
padding: 1.2rem;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
transition: all 0.3s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&.primary {
|
||||
background: linear-gradient(135deg, #3b82f6 0%, #1d4ed8 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
|
||||
.card-title, .card-subtitle {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
}
|
||||
|
||||
.card-value {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
.card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
// padding: 1rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
color: #64748b;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.card-trend {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
|
||||
&.positive {
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&.negative {
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&.stable {
|
||||
color: #7c3aed;
|
||||
}
|
||||
}
|
||||
|
||||
.card-value {
|
||||
font-size: 1.8rem;
|
||||
font-weight: bold;
|
||||
color: #1e293b;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.card-subtitle {
|
||||
color: #94a3b8;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-section {
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0 0 1rem 0;
|
||||
}
|
||||
|
||||
.trend-charts {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.trend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
|
||||
.trend-label {
|
||||
font-size: 0.85rem;
|
||||
color: #64748b;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.trend-bar {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
|
||||
.trend-fill {
|
||||
height: 100%;
|
||||
background: linear-gradient(90deg, #10b981, #059669);
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-value {
|
||||
font-size: 0.8rem;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
min-width: 40px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.center-overview {
|
||||
padding: 1rem;
|
||||
|
||||
.overview-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.overview-card {
|
||||
padding: 1rem;
|
||||
|
||||
.card-value {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.trend-charts {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.center-overview {
|
||||
.overview-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
407
my-vue-app/src/views/secondTop/components/CustomerDetail.vue
Normal file
407
my-vue-app/src/views/secondTop/components/CustomerDetail.vue
Normal file
@@ -0,0 +1,407 @@
|
||||
<template>
|
||||
<div style="width: 47vw;">
|
||||
<h2 class="section-title">客户详情</h2>
|
||||
<div id="context-panel" ref="contextPanelRef" class="section-card">
|
||||
<div v-if="selectedContact" class="context-panel-content" style="min-height: 570px;">
|
||||
<div class="panel-header">
|
||||
<h3>{{ selectedContact.name }}</h3>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, nextTick } from 'vue';
|
||||
import Chart from 'chart.js/auto';
|
||||
|
||||
// 定义props
|
||||
const props = defineProps({
|
||||
selectedContact: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
});
|
||||
|
||||
const contextPanelRef = ref(null);
|
||||
const sentimentChartCanvas = ref(null);
|
||||
const chartInstances = {};
|
||||
|
||||
// CHARTING
|
||||
const createOrUpdateChart = (chartId, canvasRef, config) => {
|
||||
if (chartInstances[chartId]) {
|
||||
chartInstances[chartId].destroy();
|
||||
}
|
||||
if (canvasRef.value) {
|
||||
const ctx = canvasRef.value.getContext('2d');
|
||||
chartInstances[chartId] = new Chart(ctx, config);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSentimentChart = (history) => {
|
||||
if (!sentimentChartCanvas.value) return;
|
||||
const ctx = sentimentChartCanvas.value.getContext('2d');
|
||||
const gradient = ctx.createLinearGradient(0, 0, 0, 120);
|
||||
gradient.addColorStop(0, 'rgba(59, 130, 246, 0.3)');
|
||||
gradient.addColorStop(1, 'rgba(59, 130, 246, 0.05)');
|
||||
|
||||
const config = {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: history.map((_, i) => `第${i+1}次`),
|
||||
datasets: [{
|
||||
label: '情绪值',
|
||||
data: history,
|
||||
borderColor: '#3b82f6',
|
||||
borderWidth: 3,
|
||||
tension: 0.4,
|
||||
fill: true,
|
||||
backgroundColor: gradient,
|
||||
pointRadius: 4,
|
||||
pointBackgroundColor: '#3b82f6',
|
||||
pointBorderColor: '#ffffff',
|
||||
pointBorderWidth: 2,
|
||||
pointHoverRadius: 6
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: { display: false },
|
||||
tooltip: {
|
||||
enabled: true,
|
||||
backgroundColor: 'rgba(0, 0, 0, 0.8)',
|
||||
titleColor: '#ffffff',
|
||||
bodyColor: '#ffffff',
|
||||
borderColor: '#3b82f6',
|
||||
borderWidth: 1,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return `情绪值: ${context.parsed.y}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
display: true,
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: {
|
||||
color: 'rgba(148, 163, 184, 0.2)',
|
||||
drawBorder: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 11
|
||||
},
|
||||
stepSize: 25
|
||||
}
|
||||
},
|
||||
x: {
|
||||
display: true,
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
color: '#64748b',
|
||||
font: {
|
||||
size: 11
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
createOrUpdateChart('sentiment', sentimentChartCanvas, config);
|
||||
};
|
||||
|
||||
// WATCHERS
|
||||
watch(() => props.selectedContact, (newContact) => {
|
||||
if (newContact && newContact.sentimentHistory && newContact.sentimentHistory.length > 0) {
|
||||
nextTick(() => {
|
||||
renderSentimentChart(newContact.sentimentHistory);
|
||||
});
|
||||
}
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// Color Palette
|
||||
$slate-50: #f8fafc;
|
||||
$slate-100: #f1f5f9;
|
||||
$slate-200: #e2e8f0;
|
||||
$slate-400: #94a3b8;
|
||||
$slate-500: #64748b;
|
||||
$slate-600: #475569;
|
||||
$slate-700: #334155;
|
||||
$slate-800: #1e293b;
|
||||
$white: #ffffff;
|
||||
|
||||
$blue: #3b82f6;
|
||||
$green: #22c55e;
|
||||
$amber: #f59e0b;
|
||||
$red: #ef4444;
|
||||
$indigo: #4f46e5;
|
||||
$purple: #a855f7;
|
||||
|
||||
h2.section-title {
|
||||
font-size: 1.25rem;
|
||||
font-weight: bold;
|
||||
color: $slate-700;
|
||||
}
|
||||
|
||||
h4 {
|
||||
font-weight: 600;
|
||||
color: $slate-700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
// Context Panel
|
||||
.section-card {
|
||||
background-color: $white;
|
||||
border-radius: 0.75rem;
|
||||
box-shadow: 0 4px 6px -1px rgba(0,0,0,0.1), 0 2px 4px -2px rgba(0,0,0,0.1);
|
||||
padding-top: 0rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.context-panel-content {
|
||||
.panel-header {
|
||||
border-bottom: 1px solid $slate-200;
|
||||
padding-bottom: 1rem;
|
||||
h3 { margin-bottom: 0; font-size: 1.25rem; }
|
||||
}
|
||||
|
||||
.detail-blocks-container {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
flex-direction: column;
|
||||
gap: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.detail-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
|
||||
&:first-child {
|
||||
flex: 1; // 左列占1份
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
flex: 2; // 右列占2份
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
&:first-child,
|
||||
&:last-child {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.detail-block {
|
||||
min-width: 0; // 防止flex项目溢出
|
||||
}
|
||||
|
||||
.form-details {
|
||||
background-color: $slate-50;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
font-size: 1rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
padding: 1rem;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
> div {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
|
||||
&.concerns {
|
||||
align-items: flex-start;
|
||||
}
|
||||
span:last-child {
|
||||
font-weight: 500;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.communication-insights {
|
||||
background-color: $slate-50;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
.insight-item {
|
||||
margin-bottom: 1rem;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
h5 {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: $slate-700;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.suggestion-tags {
|
||||
display: flex;
|
||||
gap: 0.5rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.suggestion-tag {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
padding: 0.25rem 0.5rem;
|
||||
border-radius: 0.375rem;
|
||||
|
||||
&.positive {
|
||||
background-color: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
}
|
||||
|
||||
.ratio-chart {
|
||||
.ratio-bar {
|
||||
display: flex;
|
||||
height: 1.5rem;
|
||||
border-radius: 0.375rem;
|
||||
overflow: hidden;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.speak-portion {
|
||||
background-color: #3b82f6;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
|
||||
.listen-portion {
|
||||
background-color: #22c55e;
|
||||
transition: width 0.3s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.ratio-labels {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 0.75rem;
|
||||
|
||||
.speak-label {
|
||||
color: #3b82f6;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.listen-label {
|
||||
color: #22c55e;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sentiment-summary {
|
||||
background-color: $slate-50;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.5rem;
|
||||
p { font-size: 0.875rem; font-weight: 500; span { font-weight: 400; } }
|
||||
.sentiment-chart-block {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.75rem;
|
||||
background-color: $white;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $slate-200;
|
||||
p {
|
||||
margin-bottom: 0.5rem;
|
||||
font-weight: 500;
|
||||
color: $slate-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 300px;
|
||||
color: $slate-500;
|
||||
}
|
||||
|
||||
// Timeline
|
||||
.timeline {
|
||||
border-left: 2px solid $slate-200;
|
||||
.timeline-item {
|
||||
position: relative;
|
||||
padding-left: 1rem;
|
||||
padding-bottom: 1rem;
|
||||
&:last-child {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: -6px;
|
||||
top: 5px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 9999px;
|
||||
background-color: $slate-400;
|
||||
border: 2px solid $white;
|
||||
}
|
||||
&.call::before { background-color: $blue; }
|
||||
&.email::before { background-color: $green; }
|
||||
&.meeting::before { background-color: $purple; }
|
||||
&.system::before { background-color: $slate-500; }
|
||||
}
|
||||
.timeline-date { font-size: 0.75rem; color: $slate-400; }
|
||||
.timeline-summary { font-size: 0.875rem; color: $slate-600; }
|
||||
.no-interactions { padding-left: 1rem; font-size: 0.875rem; color: $slate-400; }
|
||||
}
|
||||
|
||||
// Tags
|
||||
.tags-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
.tag {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
padding: 0.125rem 0.625rem;
|
||||
border-radius: 9999px;
|
||||
|
||||
&.concern-tag { background-color: $slate-200; color: $slate-700; }
|
||||
}
|
||||
|
||||
// Chart Containers
|
||||
.chart-container {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 180px;
|
||||
&.sentiment-chart {
|
||||
height: 120px;
|
||||
margin-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
104
my-vue-app/src/views/secondTop/components/CustomerType.vue
Normal file
104
my-vue-app/src/views/secondTop/components/CustomerType.vue
Normal file
@@ -0,0 +1,104 @@
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>客户类型占比</h3>
|
||||
<select v-model="customerTypeCategory" @change="updateChart" class="chart-select">
|
||||
<option value="age">年龄</option>
|
||||
<option value="profession">职业</option>
|
||||
<option value="childGrade">孩子年级</option>
|
||||
<option value="region">地域</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
<div ref="customerTypeChartRef" class="customer-type-chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, onMounted, onBeforeUnmount } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
|
||||
const customerTypeChartRef = ref(null);
|
||||
let customerTypeChart = null;
|
||||
|
||||
const customerTypeCategory = ref('age');
|
||||
const customerTypeData = reactive({
|
||||
age: [{ value: 120, name: '18-25岁' }, { value: 200, name: '26-35岁' }, { value: 150, name: '36-45岁' }, { value: 80, name: '46-55岁' }, { value: 50, name: '55岁以上' }],
|
||||
profession: [{ value: 180, name: '企业管理者' }, { value: 120, name: '教师' }, { value: 100, name: '医生' }, { value: 90, name: '工程师' }, { value: 110, name: '其他' }],
|
||||
childGrade: [{ value: 80, name: '幼儿园' }, { value: 150, name: '小学' }, { value: 180, name: '初中' }, { value: 120, name: '高中' }, { value: 70, name: '大学' }],
|
||||
region: [{ value: 200, name: '北京' }, { value: 150, name: '上海' }, { value: 120, name: '广州' }, { value: 100, name: '深圳' }, { value: 130, name: '其他' }]
|
||||
});
|
||||
|
||||
const updateChart = () => {
|
||||
if (!customerTypeChart) return;
|
||||
const currentData = customerTypeData[customerTypeCategory.value];
|
||||
const option = {
|
||||
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
|
||||
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
|
||||
xAxis: { type: 'category', data: currentData.map(item => item.name), axisTick: { alignWithLabel: true } },
|
||||
yAxis: { type: 'value' },
|
||||
series: [{
|
||||
name: '客户数量', type: 'bar', barWidth: '60%',
|
||||
data: currentData.map(item => item.value),
|
||||
itemStyle: {
|
||||
color: (params) => ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'][params.dataIndex % 5]
|
||||
}
|
||||
}]
|
||||
};
|
||||
customerTypeChart.setOption(option, true);
|
||||
};
|
||||
|
||||
const initChart = () => {
|
||||
if (!customerTypeChartRef.value) return;
|
||||
customerTypeChart = echarts.init(customerTypeChartRef.value);
|
||||
updateChart();
|
||||
};
|
||||
|
||||
const resizeChart = () => customerTypeChart?.resize();
|
||||
|
||||
onMounted(() => {
|
||||
initChart();
|
||||
window.addEventListener('resize', resizeChart);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
customerTypeChart?.dispose();
|
||||
window.removeEventListener('resize', resizeChart);
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.chart-container {
|
||||
background: #ffffff;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.chart-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
h3 { margin: 0; color: #303133; font-size: 18px; font-weight: 600; }
|
||||
}
|
||||
.chart-content {
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
position: relative;
|
||||
}
|
||||
.customer-type-chart {
|
||||
width: 100%;
|
||||
height: 300px;
|
||||
}
|
||||
.chart-select {
|
||||
padding: 6px 12px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid #e2e8f0;
|
||||
background-color: #f8fafc;
|
||||
font-size: 14px;
|
||||
}
|
||||
</style>
|
||||
1178
my-vue-app/src/views/secondTop/components/GoodMusic.vue
Normal file
1178
my-vue-app/src/views/secondTop/components/GoodMusic.vue
Normal file
File diff suppressed because it is too large
Load Diff
402
my-vue-app/src/views/secondTop/components/GroupComparison.vue
Normal file
402
my-vue-app/src/views/secondTop/components/GroupComparison.vue
Normal file
@@ -0,0 +1,402 @@
|
||||
<template>
|
||||
<div class="group-comparison">
|
||||
|
||||
<!-- 综合排名 -->
|
||||
<div class="ranking-section">
|
||||
<div class="ranking-header">
|
||||
<h3>综合表现排名</h3>
|
||||
<div class="manager-selector">
|
||||
<select v-model="selectedManager" @change="handleManagerChange" class="manager-dropdown">
|
||||
<option value="all">全部高级经理</option>
|
||||
<option v-for="manager in seniorManagers" :key="manager.id" :value="manager.id">
|
||||
{{ manager.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="ranking-grid compact">
|
||||
<div
|
||||
v-for="(group, index) in sortedGroups"
|
||||
:key="group.id"
|
||||
class="ranking-card"
|
||||
:class="getRankingClass(index)"
|
||||
@click="$emit('select-group', group)"
|
||||
>
|
||||
<div class="rank-badge">{{ index + 1 }}</div>
|
||||
<div class="group-info">
|
||||
<div class="group-name">{{ group.name }}</div>
|
||||
<div class="group-leader">{{ group.leader }}</div>
|
||||
</div>
|
||||
<div class="performance-score">
|
||||
<div class="score">{{ calculateScore(group) }}</div>
|
||||
<div class="score-label">综合分</div>
|
||||
</div>
|
||||
<div class="key-metrics">
|
||||
<div class="mini-metric">
|
||||
<span class="mini-label">业绩</span>
|
||||
<span class="mini-value">{{ formatCurrency(group.todayPerformance) }}</span>
|
||||
</div>
|
||||
<div class="mini-metric">
|
||||
<span class="mini-label">转化</span>
|
||||
<span class="mini-value">{{ group.conversionRate }}%</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
groups: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
|
||||
const emit = defineEmits(['select-group', 'manager-change'])
|
||||
|
||||
// 选中的高级经理
|
||||
const selectedManager = ref('all')
|
||||
|
||||
// 高级经理列表
|
||||
const seniorManagers = ref([
|
||||
{ id: 'manager1', name: '张经理' },
|
||||
{ id: 'manager2', name: '李经理' },
|
||||
{ id: 'manager3', name: '王经理' },
|
||||
{ id: 'manager4', name: '刘经理' },
|
||||
{ id: 'manager5', name: '陈经理' }
|
||||
])
|
||||
|
||||
// 处理经理选择变化
|
||||
const handleManagerChange = () => {
|
||||
emit('manager-change', selectedManager.value)
|
||||
}
|
||||
|
||||
// 按综合表现排序的组别
|
||||
const sortedGroups = computed(() => {
|
||||
return [...props.groups].sort((a, b) => calculateScore(b) - calculateScore(a))
|
||||
})
|
||||
|
||||
// 计算综合分数
|
||||
const calculateScore = (group) => {
|
||||
const performanceScore = (group.todayPerformance / 200000) * 30
|
||||
const conversionScore = (group.conversionRate / 10) * 25
|
||||
const clientScore = (group.newClients / 50) * 25
|
||||
const dealScore = (group.deals / 20) * 20
|
||||
|
||||
return Math.round(performanceScore + conversionScore + clientScore + dealScore)
|
||||
}
|
||||
|
||||
// 格式化指标值
|
||||
const formatMetricValue = (value, unit) => {
|
||||
switch (unit) {
|
||||
case 'currency':
|
||||
return formatCurrency(value)
|
||||
case 'percent':
|
||||
return value + '%'
|
||||
case 'number':
|
||||
return value.toString()
|
||||
default:
|
||||
return value
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化货币
|
||||
const formatCurrency = (value) => {
|
||||
if (value >= 10000) {
|
||||
return (value / 10000).toFixed(1) + '万'
|
||||
}
|
||||
return value.toLocaleString()
|
||||
}
|
||||
|
||||
// 获取进度条宽度
|
||||
const getProgressWidth = (value, metricKey) => {
|
||||
const maxValues = {
|
||||
todayPerformance: 200000,
|
||||
conversionRate: 10,
|
||||
newClients: 50,
|
||||
deals: 20
|
||||
}
|
||||
|
||||
return Math.min((value / maxValues[metricKey]) * 100, 100)
|
||||
}
|
||||
|
||||
// 获取表现等级样式
|
||||
const getPerformanceClass = (value, metricKey) => {
|
||||
const thresholds = {
|
||||
todayPerformance: { excellent: 120000, good: 80000 },
|
||||
conversionRate: { excellent: 6, good: 4 },
|
||||
newClients: { excellent: 25, good: 15 },
|
||||
deals: { excellent: 8, good: 5 }
|
||||
}
|
||||
|
||||
const threshold = thresholds[metricKey]
|
||||
if (value >= threshold.excellent) return 'excellent'
|
||||
if (value >= threshold.good) return 'good'
|
||||
return 'poor'
|
||||
}
|
||||
|
||||
// 获取排名样式
|
||||
const getRankingClass = (index) => {
|
||||
if (index === 0) return 'rank-1'
|
||||
if (index === 1) return 'rank-2'
|
||||
if (index === 2) return 'rank-3'
|
||||
return 'rank-other'
|
||||
}
|
||||
|
||||
// 获取趋势图标
|
||||
const getTrendIcon = (trend) => {
|
||||
const icons = {
|
||||
up: '↗',
|
||||
down: '↘',
|
||||
stable: '→'
|
||||
}
|
||||
return icons[trend] || '→'
|
||||
}
|
||||
|
||||
// 获取预警图标
|
||||
const getAlertIcon = (level) => {
|
||||
const icons = {
|
||||
warning: '⚠️',
|
||||
info: 'ℹ️',
|
||||
urgent: '🚨'
|
||||
}
|
||||
return icons[level] || 'ℹ️'
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-comparison {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
height: 24rem;
|
||||
overflow: auto;
|
||||
|
||||
// 综合排名
|
||||
.ranking-section {
|
||||
// margin-bottom: 2rem;
|
||||
|
||||
.ranking-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
h3 {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 600;
|
||||
color: #374151;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.manager-selector {
|
||||
.manager-dropdown {
|
||||
padding: 0.5rem 0.75rem;
|
||||
border: 1px solid #d1d5db;
|
||||
border-radius: 6px;
|
||||
background: white;
|
||||
font-size: 0.875rem;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
border-color: #9ca3af;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
|
||||
gap: 1rem;
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: repeat(1, 1fr);
|
||||
gap: 0.75rem;
|
||||
|
||||
.ranking-card {
|
||||
padding: 0.75rem;
|
||||
gap: 0.5rem;
|
||||
|
||||
.rank-badge {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.group-info {
|
||||
.group-name {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.group-leader {
|
||||
font-size: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.performance-score {
|
||||
.score {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
}
|
||||
|
||||
.key-metrics {
|
||||
.mini-metric {
|
||||
.mini-label {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.mini-value {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
&.rank-1 {
|
||||
background: linear-gradient(135deg, #ffd700 0%, #ffed4e 100%);
|
||||
border: 2px solid #f59e0b;
|
||||
}
|
||||
|
||||
&.rank-2 {
|
||||
background: linear-gradient(135deg, #c0c0c0 0%, #e5e7eb 100%);
|
||||
border: 2px solid #9ca3af;
|
||||
}
|
||||
|
||||
&.rank-3 {
|
||||
background: linear-gradient(135deg, #cd7f32 0%, #d97706 100%);
|
||||
border: 2px solid #b45309;
|
||||
}
|
||||
|
||||
&.rank-other {
|
||||
background: white;
|
||||
border: 1px solid #e5e7eb;
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
margin-right: 1rem;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.group-info {
|
||||
flex: 1;
|
||||
|
||||
.group-name {
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.group-leader {
|
||||
font-size: 0.85rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.performance-score {
|
||||
text-align: center;
|
||||
margin: 0 1rem;
|
||||
|
||||
.score {
|
||||
font-size: 1.5rem;
|
||||
font-weight: bold;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
.score-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.key-metrics {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
|
||||
.mini-metric {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.5rem;
|
||||
|
||||
.mini-label {
|
||||
font-size: 0.75rem;
|
||||
color: #6b7280;
|
||||
}
|
||||
|
||||
.mini-value {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.group-comparison {
|
||||
padding: 0.75rem;
|
||||
|
||||
.ranking-grid {
|
||||
grid-template-columns: 1fr;
|
||||
|
||||
&.compact {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-card {
|
||||
.key-metrics {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
331
my-vue-app/src/views/secondTop/components/GroupRanking.vue
Normal file
331
my-vue-app/src/views/secondTop/components/GroupRanking.vue
Normal file
@@ -0,0 +1,331 @@
|
||||
<template>
|
||||
<div class="group-ranking">
|
||||
<div class="ranking-header">
|
||||
<h2>各阶段转化率 vs. 公司平均</h2>
|
||||
<div class="legend">
|
||||
<div class="legend-item">
|
||||
<div class="legend-color team"></div>
|
||||
<span>本团队</span>
|
||||
</div>
|
||||
<div class="legend-item">
|
||||
<div class="legend-color company"></div>
|
||||
<span>公司平均</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-container">
|
||||
<div class="chart">
|
||||
<div class="chart-main">
|
||||
<div class="y-axis">
|
||||
<div class="y-label" v-for="label in yAxisLabels" :key="label">{{ label }}</div>
|
||||
</div>
|
||||
|
||||
<div class="chart-content">
|
||||
<div class="chart-item" v-for="stage in conversionStages" :key="stage.name">
|
||||
<div class="bars">
|
||||
<div class="bar-group">
|
||||
<div class="bar-container">
|
||||
<div class="bar-value team-value">{{ stage.teamRate }}%</div>
|
||||
<div
|
||||
class="bar team-bar"
|
||||
:style="{ height: (stage.teamRate * 2.4) + 'px' }"
|
||||
:title="`本团队: ${stage.teamRate}%`"
|
||||
></div>
|
||||
</div>
|
||||
<div class="bar-container">
|
||||
<div class="bar-value company-value">{{ stage.companyRate }}%</div>
|
||||
<div
|
||||
class="bar company-bar"
|
||||
:style="{ height: (stage.companyRate * 2.4) + 'px' }"
|
||||
:title="`公司平均: ${stage.companyRate}%`"
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- X轴刻度 -->
|
||||
<div class="x-axis">
|
||||
<div class="x-label" v-for="stage in conversionStages" :key="stage.name">
|
||||
{{ stage.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
selectedGroup: {
|
||||
type: Object,
|
||||
default: null
|
||||
}
|
||||
})
|
||||
|
||||
// 转化率数据
|
||||
const conversionStages = ref([
|
||||
{
|
||||
name: '加微',
|
||||
teamRate: 80,
|
||||
companyRate: 85
|
||||
},
|
||||
{
|
||||
name: '填表',
|
||||
teamRate: 90,
|
||||
companyRate: 92
|
||||
},
|
||||
{
|
||||
name: '通话',
|
||||
teamRate: 95,
|
||||
companyRate: 95
|
||||
},
|
||||
{
|
||||
name: '首课',
|
||||
teamRate: 60,
|
||||
companyRate: 65
|
||||
},
|
||||
{
|
||||
name: '三课',
|
||||
teamRate: 85,
|
||||
companyRate: 88
|
||||
},
|
||||
{
|
||||
name: '付费',
|
||||
teamRate: 15,
|
||||
companyRate: 20
|
||||
}
|
||||
])
|
||||
|
||||
// Y轴标签
|
||||
const yAxisLabels = ref(['100%', '80%', '60%', '40%', '20%', '0%'])
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.group-ranking {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 1.5rem;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
height: 23rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ranking-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1.5rem;
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
|
||||
.legend-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.9rem;
|
||||
color: #64748b;
|
||||
|
||||
.legend-color {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 4px;
|
||||
|
||||
&.team {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
&.company {
|
||||
background: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
flex: 1;
|
||||
|
||||
.chart {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 320px;
|
||||
|
||||
.chart-main {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.y-axis {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
width: 40px;
|
||||
padding-right: 10px;
|
||||
|
||||
.y-label {
|
||||
font-size: 0.8rem;
|
||||
color: #64748b;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-around;
|
||||
border-left: 1px solid #e2e8f0;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 0 1rem 0 1rem;
|
||||
position: relative;
|
||||
|
||||
|
||||
|
||||
.chart-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
|
||||
.bars {
|
||||
height: 240px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: center;
|
||||
|
||||
.bar-group {
|
||||
display: flex;
|
||||
gap: 1px;
|
||||
align-items: flex-end;
|
||||
|
||||
.bar-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.bar-value {
|
||||
font-size: 0.75rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 4px;
|
||||
min-height: 16px;
|
||||
|
||||
&.team-value {
|
||||
color: #3b82f6;
|
||||
}
|
||||
|
||||
&.company-value {
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 20px;
|
||||
min-height: 2px;
|
||||
border-radius: 2px 2px 0 0;
|
||||
transition: all 0.3s ease;
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
&.team-bar {
|
||||
background: #3b82f6;
|
||||
}
|
||||
|
||||
&.company-bar {
|
||||
background: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.x-axis {
|
||||
display: flex;
|
||||
padding: 0.5rem 0 0 41px;
|
||||
|
||||
.x-label {
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
font-size: 0.9rem;
|
||||
color: #374151;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// 移动端适配
|
||||
@media (max-width: 768px) {
|
||||
.group-ranking {
|
||||
.ranking-header {
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
align-items: flex-start;
|
||||
|
||||
h2 {
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-container {
|
||||
.chart {
|
||||
height: 270px;
|
||||
|
||||
.chart-main .chart-content {
|
||||
.chart-item {
|
||||
.bars {
|
||||
height: 180px;
|
||||
|
||||
.bar-group {
|
||||
gap: 6px;
|
||||
|
||||
.bar-container {
|
||||
.bar-value {
|
||||
font-size: 0.7rem;
|
||||
}
|
||||
|
||||
.bar {
|
||||
width: 16px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.x-axis {
|
||||
padding: 0.5rem 0 0 31px;
|
||||
|
||||
.x-label {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
222
my-vue-app/src/views/secondTop/components/ProblemRanking.vue
Normal file
222
my-vue-app/src/views/secondTop/components/ProblemRanking.vue
Normal file
@@ -0,0 +1,222 @@
|
||||
<template>
|
||||
<div class="chart-container">
|
||||
<div class="chart-header">
|
||||
<h3>客户迫切解决的问题排行榜</h3>
|
||||
</div>
|
||||
<div class="chart-content">
|
||||
<div v-if="sortedData.length > 0" class="problem-ranking">
|
||||
<div
|
||||
v-for="(item, index) in sortedData"
|
||||
:key="item.name"
|
||||
class="ranking-item"
|
||||
:class="getRankingClass(index)"
|
||||
>
|
||||
<div class="rank-number">
|
||||
<span class="rank-badge" :class="getRankBadgeClass(index)">{{ index + 1 }}</span>
|
||||
</div>
|
||||
<div class="problem-info">
|
||||
<div class="problem-name">{{ item.name }}</div>
|
||||
</div>
|
||||
<div class="problem-percentage">
|
||||
<span class="percentage">{{ getPercentage(item.value) }}%</span>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" :style="{ width: getPercentage(item.value) + '%' }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="empty-state">
|
||||
<p>暂无排行榜数据</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue';
|
||||
|
||||
// 定义Props,接收一个包含 { name: string, value: string | number } 的数组
|
||||
const props = defineProps({
|
||||
rankingData: {
|
||||
type: Array,
|
||||
default: () => [] // 默认值为空数组
|
||||
}
|
||||
});
|
||||
|
||||
// --- 计算属性 ---
|
||||
// 对传入的数据进行排序
|
||||
const sortedData = computed(() => {
|
||||
if (Array.isArray(props.rankingData) && props.rankingData.length > 0) {
|
||||
// 创建一个副本进行排序,以避免直接修改 props
|
||||
return [...props.rankingData].sort((a, b) => {
|
||||
// 统一将值转换为数字进行比较
|
||||
const aValue = parseFloat(String(a.value).replace('%', '')) || 0;
|
||||
const bValue = parseFloat(String(b.value).replace('%', '')) || 0;
|
||||
return bValue - aValue;
|
||||
});
|
||||
}
|
||||
return []; // 如果没有有效数据,返回空数组
|
||||
});
|
||||
|
||||
// --- 辅助方法 ---
|
||||
// 获取百分比数值,兼容 "55%" 和 55 两种格式
|
||||
const getPercentage = (value) => {
|
||||
return parseFloat(String(value).replace('%', '')) || 0;
|
||||
};
|
||||
|
||||
// 根据排名索引返回不同的CSS类
|
||||
const getRankingClass = (index) => {
|
||||
return ['rank-first', 'rank-second', 'rank-third'][index] || 'rank-other';
|
||||
};
|
||||
|
||||
// 根据排名索引返回徽章的CSS类
|
||||
const getRankBadgeClass = (index) => {
|
||||
return ['badge-gold', 'badge-silver', 'badge-bronze'][index] || 'badge-default';
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
/* 外部容器样式 */
|
||||
.chart-container {
|
||||
background: white;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.05);
|
||||
height: 400px; /* 保持与其他卡片高度一致 */
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.chart-header {
|
||||
padding: 20px 20px 16px;
|
||||
border-bottom: 1px solid #ebeef5;
|
||||
h3 {
|
||||
margin: 0;
|
||||
color: #303133;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.chart-content {
|
||||
padding: 20px;
|
||||
flex-grow: 1;
|
||||
overflow: hidden; /* 防止内容溢出 */
|
||||
}
|
||||
|
||||
/* 排行榜核心样式 */
|
||||
.problem-ranking {
|
||||
overflow-y: auto;
|
||||
/* 自定义滚动条样式 (可选) */
|
||||
&::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #ccc;
|
||||
border-radius: 3px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
}
|
||||
|
||||
.ranking-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 8px;
|
||||
border-radius: 8px;
|
||||
background: #f8f9fa;
|
||||
border: 1px solid #e9ecef;
|
||||
transition: all 0.3s ease;
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
.rank-number {
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.rank-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
font-weight: bold;
|
||||
font-size: 14px;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
&.badge-gold {
|
||||
background: linear-gradient(135deg, #ffd700, #ffb300);
|
||||
}
|
||||
&.badge-silver {
|
||||
background: linear-gradient(135deg, #c0c0c0, #a8a8a8);
|
||||
}
|
||||
&.badge-bronze {
|
||||
background: linear-gradient(135deg, #cd7f32, #b8860b);
|
||||
}
|
||||
&.badge-default {
|
||||
background: linear-gradient(135deg, #6c757d, #495057);
|
||||
}
|
||||
}
|
||||
|
||||
.problem-info {
|
||||
flex: 1;
|
||||
margin-right: 16px;
|
||||
}
|
||||
|
||||
.problem-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #212529;
|
||||
}
|
||||
|
||||
.problem-percentage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-end;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #495057;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.progress-bar {
|
||||
width: 60px;
|
||||
height: 6px;
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s ease;
|
||||
|
||||
.rank-first & { background: linear-gradient(90deg, #ffd700, #ffb300); }
|
||||
.rank-second & { background: linear-gradient(90deg, #c0c0c0, #a8a8a8); }
|
||||
.rank-third & { background: linear-gradient(90deg, #cd7f32, #b8860b); }
|
||||
.rank-other & { background: linear-gradient(90deg, #007bff, #0056b3); }
|
||||
}
|
||||
|
||||
/* 空状态样式 */
|
||||
.empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
color: #909399;
|
||||
font-size: 16px;
|
||||
}
|
||||
</style>
|
||||
1117
my-vue-app/src/views/secondTop/components/seniorManager.vue
Normal file
1117
my-vue-app/src/views/secondTop/components/seniorManager.vue
Normal file
File diff suppressed because it is too large
Load Diff
1262
my-vue-app/src/views/secondTop/secondTop.vue
Normal file
1262
my-vue-app/src/views/secondTop/secondTop.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user