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

添加了Vue3项目基础结构,包括路由、状态管理和API配置
实现了销售管理系统的核心功能模块,包括业绩看板、团队管理和客户分析
集成了Element Plus UI组件库和ECharts数据可视化
添加了全局样式和响应式布局支持
This commit is contained in:
2025-08-06 20:20:35 +08:00
commit 9af92a7975
66 changed files with 29082 additions and 0 deletions

View File

@@ -0,0 +1,252 @@
<template>
<div class="camp-management-board">
<header class="board-header">
<div>
<h1>营期节奏总览与调控</h1>
<p>点击任意中心行即可展开或收起详情调控面板</p>
</div>
<button class="save-button" @click="saveSettings">保存全部设置</button>
</header>
<div class="overview-panel">
<!-- 列表头部 -->
<div class="overview-header">
<span class="header-name">中心名称</span>
<span class="header-stage">当前营期阶段</span>
<span class="header-timeline">营期节奏分布</span>
<span class="header-days">总天数</span>
</div>
<!-- 中心列表 -->
<div class="center-list">
<template v-for="center in centersData" :key="center.id">
<!-- 1. 概览行 (可点击) -->
<div
class="center-summary-row"
@click="selectCenter(center.id)"
:class="{ 'selected': selectedCenterId === center.id }"
>
<span class="center-name">{{ center.name }}</span>
<span class="current-stage">{{ getCurrentStage(center) }}</span>
<!-- 可视化时间轴 -->
<div class="timeline-bar">
<div
v-for="stage in center.stages"
:key="stage.name"
class="timeline-segment"
:style="getStageStyle(center, stage)"
:title="`${stage.name}: ${stage.days} 天`"
>
<span class="stage-label" v-if="(stage.days / calculateTotalDays(center)) > 0.08">{{ stage.name }}</span>
</div>
</div>
<span class="total-days">{{ calculateTotalDays(center) }} </span>
</div>
<!-- 2. 详情调控面板 (条件渲染带过渡动画) -->
<transition name="slide-fade">
<div v-if="selectedCenterId === center.id" class="detail-control-panel">
<h4>正在调控: {{ center.name }}</h4>
<ul class="control-items-container">
<li v-for="stage in center.stages" :key="stage.name" class="control-item">
<span class="color-dot" :style="{ backgroundColor: stage.color }"></span>
<span class="stage-name-detail">{{ stage.name }}</span>
<input type="number" v-model.number="stage.days" min="0" class="days-input" />
<span></span>
</li>
</ul>
</div>
</transition>
</template>
</div>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue';
// 初始数据模型保持不变
const centersData = ref([
{ id: 1, name: '一中心', startDate: '2025-08-1', stages: [ { name: '接数据', days: 3, color: '#ffc107' }, { name: '课一', days: 1, color: '#0dcaf0' }, { name: '课二', days: 1, color: '#0d6efd' }, { name: '课三', days: 1, color: '#6f42c1' }, { name: '课四', days: 1, color: '#d63384' }] },
{ id: 2, name: '二中心', startDate: '2025-08-3', stages: [ { name: '接数据', days: 2, color: '#ffc107' }, { name: '课一', days: 1, color: '#0dcaf0' }, { name: '课二', days: 1, color: '#0d6efd' }, { name: '课三', days: 1, color: '#6f42c1' }, { name: '课四', days: 1, color: '#d63384' }] },
{ id: 3, name: '三中心', startDate: '2025-08-5', stages: [ { name: '接数据', days: 4, color: '#ffc107' }, { name: '课一', days: 1, color: '#0dcaf0' }, { name: '课二', days: 1, color: '#0d6efd' }, { name: '课三', days: 1, color: '#6f42c1' }, { name: '课四', days: 1, color: '#d63384' }] },
{ id: 4, name: '四中心', startDate: '2025-08-5', stages: [ { name: '接数据', days: 2, color: '#ffc107' }, { name: '课一', days: 1, color: '#0dcaf0' }, { name: '课二', days: 1, color: '#0d6efd' }, { name: '课三', days: 1, color: '#6f42c1' }, { name: '课四', days: 1, color: '#d63384' }] },
{ id: 5, name: '五中心', startDate: '2025-08-4', stages: [ { name: '接数据', days: 3, color: '#ffc107' }, { name: '课一', days: 1, color: '#0dcaf0' }, { name: '课二', days: 1, color: '#0d6efd' }, { name: '课三', days: 1, color: '#6f42c1' }, { name: '课四', days: 1, color: '#d63384' }] },
]);
// 新增状态用于追踪当前被选中的中心ID
const selectedCenterId = ref(null);
const getCurrentStage = (center) => {
const today = new Date();
today.setHours(0, 0, 0, 0);
const startDate = new Date(center.startDate);
startDate.setHours(0, 0, 0, 0);
if (today < startDate) {
return '未开始';
}
const diffTime = Math.abs(today - startDate);
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)) + 1; // 当天算第一天
let cumulativeDays = 0;
for (let i = 0; i < center.stages.length; i++) {
const stage = center.stages[i];
cumulativeDays += stage.days;
if (diffDays <= cumulativeDays) {
return `${stage.name}`;
}
}
return '已结束';
};
/**
* 选择一个中心进行调控
* 如果点击的是已选中的中心,则取消选择(收起面板)
* @param {number} centerId - 被点击的中心ID
*/
const selectCenter = (centerId) => {
if (selectedCenterId.value === centerId) {
selectedCenterId.value = null; // 取消选择
} else {
selectedCenterId.value = centerId; // 选择新的
}
};
// 以下函数与之前版本基本相同
const calculateTotalDays = (center) => {
return center.stages.reduce((sum, stage) => sum + (Number(stage.days) || 0), 0);
};
const getStageStyle = (center, stage) => {
const totalDays = calculateTotalDays(center);
const widthPercentage = totalDays > 0 ? (stage.days / totalDays) * 100 : 0;
return {
width: `${widthPercentage}%`,
backgroundColor: stage.color,
};
};
const saveSettings = () => {
console.log('正在保存设置...');
alert('所有中心的设置已保存!请在浏览器控制台查看最新数据。');
console.log('最新的营期数据:', JSON.parse(JSON.stringify(centersData.value)));
};
</script>
<style scoped>
/* 整体面板 */
.camp-management-board {
font-family: 'Helvetica Neue', Arial, sans-serif;
padding: 24px;
background-color: #f4f7fa;
color: #333;
}
.board-header {
/* margin-bottom: 24px; */
display: flex;
justify-content: space-between;
align-items: center;
}
.board-header h1 { margin: 0; font-size: 20px; color: #2c3e50; }
.board-header p { color: #7f8c8d; margin: 8px 0 16px; }
.save-button { padding: 10px 20px; font-size: 14px; font-weight: bold; color: #fff; background-color: #4caf50; border: none; border-radius: 8px; cursor: pointer; transition: background-color 0.3s ease; }
.save-button:hover { background-color: #45a049; }
/* 总览面板 */
.overview-panel {
background-color: #fff;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
overflow: hidden; /* 保证内部元素不超出圆角 */
}
.overview-header {
display: flex;
align-items: center;
padding: 12px 24px;
background-color: #f8f9fa;
color: #6c757d;
font-weight: 600;
font-size: 14px;
border-bottom: 1px solid #e9ecef;
}
.header-name { width: 15%; }
.header-stage { width: 15%; text-align: center; }
.header-timeline { flex: 1; text-align: center; }
.header-days { width: 10%; text-align: right; }
/* 中心概览行 */
.center-summary-row {
display: flex;
align-items: center;
padding: 16px 24px;
border-bottom: 1px solid #e9ecef;
cursor: pointer;
transition: background-color 0.3s ease;
}
.center-summary-row:last-child {
border-bottom: none;
}
.center-summary-row:hover {
background-color: #f1f3f5;
}
.center-summary-row.selected {
background-color: #e7f5ff; /* 选中时的背景色 */
border-left: 4px solid #1c7ed6; /* 选中时左侧的强调线 */
padding-left: 20px;
}
.center-name { width: 15%; font-size: 18px; font-weight: 500; color: #34495e; }
.current-stage { width: 15%; text-align: center; font-size: 16px; font-weight: 500; color: #28a745; }
.timeline-bar { flex: 1; display: flex; height: 32px; border-radius: 6px; background-color: #e9ecef; overflow: hidden; margin: 0 20px; }
.timeline-segment { height: 100%; display: flex; align-items: center; justify-content: center; color: white; font-size: 12px; font-weight: bold; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; transition: width 0.4s cubic-bezier(0.25, 0.8, 0.25, 1); }
.total-days { width: 10%; text-align: right; font-size: 18px; font-weight: bold; color: #e67e22; }
/* 详情调控面板 */
.detail-control-panel {
/* padding: 20px 24px 12px 24px; */
background-color: #fafafa;
border-bottom: 1px solid #e9ecef
}
.detail-control-panel h4 { margin-top: 0; margin-bottom: 16px; font-size: 16px; color: #1c7ed6; }
.detail-control-panel .control-items-container {
list-style: none;
padding: 0;
margin: 0;
display: flex;
/* flex-wrap: wrap; */
}
.control-item {
display: flex;
align-items: center;
margin-bottom: 10px;
margin-right: 15px;
}
.color-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; }
.stage-name-detail {width: 50px; font-size: 16px; flex-shrink: 0; }
.days-input { width: 70px; padding: 8px; border: 1px solid #ccc; border-radius: 6px; text-align: center; font-size: 16px; margin-right: 8px; }
.days-input:focus { outline: none; border-color: #0d6efd; box-shadow: 0 0 0 3px rgba(13, 110, 253, 0.25); }
/* 过渡动画 */
.slide-fade-enter-active {
transition: all 0.3s ease-out;
}
.slide-fade-leave-active {
transition: all 0.3s cubic-bezier(1, 0.5, 0.8, 1);
}
.slide-fade-enter-from,
.slide-fade-leave-to {
transform: translateY(-10px);
opacity: 0;
}
</style>

View File

@@ -0,0 +1,104 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>沟通总数据</h3>
<span class="metric-period">本周</span>
</div>
<div class="communication-cards">
<div class="comm-card">
<div class="card-icon">📞</div>
<div class="card-content">
<div class="card-label">总通话时长</div>
<div class="card-value">{{ communicationData.totalDuration }}小时</div>
</div>
</div>
<div class="comm-card">
<div class="card-icon"></div>
<div class="card-content">
<div class="card-label">有效沟通率</div>
<div class="card-value">{{ communicationData.effectiveRate }}%</div>
</div>
</div>
<div class="comm-card">
<div class="card-icon"></div>
<div class="card-content">
<div class="card-label">首次响应时长</div>
<div class="card-value">{{ communicationData.firstResponseTime }}</div>
</div>
</div>
<div class="comm-card">
<div class="card-icon">📊</div>
<div class="card-content">
<div class="card-label">接通率</div>
<div class="card-value">{{ communicationData.connectionRate }}%</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
communicationData: Object
});
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.metric-period {
font-size: 14px;
color: #666;
}
.communication-cards {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 15px;
}
.comm-card {
display: flex;
align-items: center;
background-color: #f9f9f9;
padding: 15px;
border-radius: 8px;
}
.card-icon {
font-size: 24px;
margin-right: 15px;
}
.card-label {
font-size: 14px;
color: #666;
}
.card-value {
font-size: 18px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,119 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>客户画像</h3>
</div>
<div class="customer-profile">
<div class="profile-section">
<h4>家长类型分布</h4>
<div class="parent-types">
<div v-for="type in parentTypes" :key="type.name" class="parent-type-item">
<div class="type-info">
<span class="type-name">{{ type.name }}</span>
<span class="type-percentage">{{ type.percentage }}%</span>
</div>
<div class="type-bar">
<div class="type-fill" :style="{ width: type.percentage + '%', backgroundColor: type.color }"></div>
</div>
</div>
</div>
</div>
<div class="profile-section" style="margin-bottom: 25px;">
<h4>热门问题排行</h4>
<div class="hot-questions">
<div v-for="(question, index) in hotQuestions.slice(0, 3)" :key="question.id" class="question-item">
<div class="question-rank">{{ index + 1 }}</div>
<div class="question-content">
<div class="question-text">{{ question.text }}</div>
<div class="question-count">{{ question.count }}次咨询</div>
</div>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
parentTypes: Array,
hotQuestions: Array
});
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
max-height: 300px;
overflow-y: auto;
/* flex: 1; */
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.customer-profile .profile-section h4 {
font-size: 16px;
margin-bottom: 15px;
}
.parent-type-item {
margin-bottom: 10px;
}
.type-info {
display: flex;
justify-content: space-between;
margin-bottom: 5px;
font-size: 14px;
}
.type-bar {
background-color: #f0f0f0;
border-radius: 4px;
height: 8px;
}
.type-fill {
height: 100%;
border-radius: 4px;
}
.hot-questions .question-item {
display: flex;
align-items: center;
margin-bottom: 10px;
}
.question-rank {
font-size: 16px;
font-weight: bold;
width: 30px;
text-align: center;
}
.question-text {
font-size: 14px;
}
.question-count {
font-size: 12px;
color: #666;
}
</style>

View File

@@ -0,0 +1,93 @@
<template>
<div class="dashboard-card detail-section">
<div class="card-header">
<h3>数据详情</h3>
</div>
<div class="detail-content">
<div v-if="!selectedPerson" class="no-selection">
<div class="empty-icon">📊</div>
<p>请点击左侧表格中的人员查看详细数据</p>
</div>
<div v-else class="person-detail">
<div class="detail-header">
<div class="detail-avatar">{{ selectedPerson.name.charAt(0) }}</div>
<div class="detail-info">
<h4>{{ selectedPerson.name }}</h4>
<p>{{ selectedPerson.position }} - {{ selectedPerson.department }}</p>
</div>
</div>
<div class="detail-placeholder">
<p>详细数据面板</p>
<p class="placeholder-text">此处将显示选中人员的详细数据分析包括业绩趋势客户分析等信息</p>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
selectedPerson: Object
});
</script>
<style scoped>
.dashboard-card.detail-section {
grid-column: 3 / 4;
}
.detail-content .no-selection {
text-align: center;
padding: 40px 20px;
color: #999;
}
.no-selection .empty-icon {
font-size: 48px;
margin-bottom: 20px;
}
.person-detail .detail-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.detail-avatar {
width: 60px;
height: 60px;
border-radius: 50%;
background-color: #2196F3;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
font-weight: bold;
margin-right: 20px;
}
.detail-info h4 {
margin: 0;
font-size: 20px;
}
.detail-info p {
margin: 0;
color: #666;
}
.detail-placeholder {
text-align: center;
padding: 20px;
background-color: #f9f9f9;
border-radius: 8px;
}
.placeholder-text {
font-size: 14px;
color: #999;
}
</style>

View File

@@ -0,0 +1,233 @@
<template>
<div class="dashboard-card table-section">
<div class="card-header">
<h3>详细数据表格</h3>
</div>
<div class="data-table-container">
<!-- 筛选器 -->
<div class="table-filters">
<div class="filter-group">
<label>部门:</label>
<select v-model="filters.department">
<option value="">全部部门</option>
<option value="销售一部">销售一部</option>
<option value="销售二部">销售二部</option>
<option value="销售三部">销售三部</option>
</select>
</div>
<div class="filter-group">
<label>职位:</label>
<select v-model="filters.position">
<option value="">全部职位</option>
<option value="销售经理">销售经理</option>
<option value="销售专员">销售专员</option>
<option value="销售助理">销售助理</option>
</select>
</div>
<div class="filter-group">
<label>时间范围:</label>
<select v-model="filters.timeRange">
<option value="today">今日</option>
<option value="week">本周</option>
<option value="month">本月</option>
<option value="quarter">本季度</option>
</select>
</div>
<div class="filter-group">
<label>成交状态:</label>
<select v-model="filters.dealStatus">
<option value="">全部状态</option>
<option value="已成交">已成交</option>
<option value="跟进中">跟进中</option>
<option value="已失效">已失效</option>
</select>
</div>
</div>
<!-- 数据表格 -->
<div class="data-table">
<table>
<thead>
<tr>
<th>人员</th>
<th @click="$emit('sort-by', 'dealRate')" class="sortable">
成交率
<span class="sort-icon" :class="{ active: sortField === 'dealRate' }">
{{ sortOrder === 'desc' ? '↓' : '↑' }}
</span>
</th>
<th @click="$emit('sort-by', 'callDuration')" class="sortable">
通话时长
<span class="sort-icon" :class="{ active: sortField === 'callDuration' }">
{{ sortOrder === 'desc' ? '↓' : '↑' }}
</span>
</th>
<th>通话次数</th>
<th>成交金额</th>
<th>部门</th>
</tr>
</thead>
<tbody>
<tr v-for="person in filteredTableData" :key="person.id" @click="$emit('select-person', person)" :class="{ selected: selectedPerson && selectedPerson.id === person.id }">
<td>
<div class="person-info">
<div class="person-avatar">{{ person.name.charAt(0) }}</div>
<div>
<div class="person-name">{{ person.name }}</div>
<div class="person-position">{{ person.position }}</div>
</div>
</div>
</td>
<td>
<div class="deal-rate">
<span class="rate-value" :class="getRateClass(person.dealRate)">{{ person.dealRate }}%</span>
<div class="rate-bar">
<div class="rate-fill" :style="{ width: person.dealRate + '%', backgroundColor: getRateColor(person.dealRate) }"></div>
</div>
</div>
</td>
<td>{{ formatDuration(person.callDuration) }}</td>
<td>{{ person.callCount }}</td>
<td>¥{{ person.dealAmount.toLocaleString() }}</td>
<td>{{ person.department }}</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, reactive } from 'vue';
defineProps({
filteredTableData: Array,
selectedPerson: Object,
sortField: String,
sortOrder: String,
getRateClass: Function,
getRateColor: Function,
formatDuration: Function
});
defineEmits(['sort-by', 'select-person']);
const filters = reactive({
department: '',
position: '',
timeRange: 'month',
dealStatus: ''
});
</script>
<style scoped>
.dashboard-card.table-section {
grid-column: 1 / 3;
}
.data-table-container {
margin-top: 20px;
}
.table-filters {
display: flex;
gap: 20px;
margin-bottom: 20px;
}
.filter-group {
display: flex;
align-items: center;
gap: 8px;
}
.filter-group label {
font-size: 14px;
}
.filter-group select {
padding: 6px 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.data-table table {
width: 100%;
border-collapse: collapse;
}
.data-table th, .data-table td {
padding: 12px 15px;
text-align: left;
border-bottom: 1px solid #eee;
}
.data-table th.sortable {
cursor: pointer;
}
.sort-icon {
opacity: 0.5;
}
.sort-icon.active {
opacity: 1;
}
.person-info {
display: flex;
align-items: center;
}
.person-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background-color: #4CAF50;
color: white;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
margin-right: 10px;
}
.person-name {
font-weight: bold;
}
.person-position {
font-size: 12px;
color: #666;
}
.deal-rate .rate-value {
font-weight: bold;
}
.deal-rate .rate-bar {
height: 6px;
background-color: #f0f0f0;
border-radius: 3px;
margin-top: 5px;
}
.deal-rate .rate-fill {
height: 100%;
border-radius: 3px;
}
tbody tr {
cursor: pointer;
transition: background-color 0.2s;
}
tbody tr:hover {
background-color: #f5f5f5;
}
tbody tr.selected {
background-color: #e8f5e9;
}
</style>

View File

@@ -0,0 +1,80 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>转化漏斗</h3>
</div>
<div class="funnel-chart">
<div v-for="(stage, index) in funnelData" :key="stage.name" class="funnel-stage">
<div class="stage-bar" :style="{ width: stage.percentage + '%', backgroundColor: stage.color }">
<span class="stage-label">{{ stage.name }}</span>
<span class="stage-count">{{ stage.count }}</span>
</div>
<div class="stage-percentage">{{ stage.percentage }}%</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
funnelData: Array
});
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.funnel-chart {
display: flex;
flex-direction: column;
gap: 10px;
}
.funnel-stage {
display: flex;
align-items: center;
}
.stage-bar {
height: 30px;
line-height: 30px;
color: white;
padding: 0 10px;
border-radius: 4px;
display: flex;
justify-content: space-between;
align-items: center;
transition: width 0.5s ease-in-out;
}
.stage-label {
font-weight: bold;
}
.stage-percentage {
margin-left: 10px;
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,107 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>核心业绩指标</h3>
<span class="metric-period">本月</span>
</div>
<div class="kpi-metrics">
<div class="kpi-item">
<div class="kpi-label">销售额</div>
<div class="kpi-value">¥{{ formatNumber(kpiData.salesAmount) }}</div>
<div class="kpi-trend" :class="kpiData.salesTrend > 0 ? 'positive' : 'negative'">
{{ kpiData.salesTrend > 0 ? '+' : '' }}{{ kpiData.salesTrend }}%
</div>
</div>
<div class="kpi-item">
<div class="kpi-label">成交客户</div>
<div class="kpi-value">{{ kpiData.dealCustomers }}</div>
<div class="kpi-trend" :class="kpiData.customerTrend > 0 ? 'positive' : 'negative'">
{{ kpiData.customerTrend > 0 ? '+' : '' }}{{ kpiData.customerTrend }}%
</div>
</div>
<div class="kpi-item">
<div class="kpi-label">转化率</div>
<div class="kpi-value">{{ kpiData.conversionRate }}%</div>
<div class="kpi-trend" :class="kpiData.conversionTrend > 0 ? 'positive' : 'negative'">
{{ kpiData.conversionTrend > 0 ? '+' : '' }}{{ kpiData.conversionTrend }}%
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
kpiData: Object,
formatNumber: Function
});
function formatNumber(num) {
return num.toLocaleString();
}
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.metric-period {
font-size: 14px;
color: #666;
}
.kpi-metrics {
display: flex;
justify-content: space-around;
text-align: center;
}
.kpi-item {
flex: 1;
}
.kpi-label {
font-size: 14px;
color: #666;
margin-bottom: 5px;
}
.kpi-value {
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
}
.kpi-trend {
font-size: 14px;
}
.kpi-trend.positive {
color: #4CAF50;
}
.kpi-trend.negative {
color: #F44336;
}
</style>

View File

@@ -0,0 +1,101 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>优质通话</h3>
<button class="view-all-btn">查看全部</button>
</div>
<div class="quality-calls">
<div v-for="call in qualityCalls.slice(0, 3)" :key="call.id" class="call-item">
<div class="call-info">
<div class="caller-name">{{ call.callerName }}</div>
<div class="call-details">
<span class="duration">{{ call.duration }}分钟</span>
<span class="score">评分: {{ call.score }}/10</span>
</div>
</div>
<div class="call-actions">
<button class="play-btn" @click="$emit('play-call', call.id)">
<i class="icon-play"></i>
</button>
<button class="download-btn" @click="$emit('download-call', call.id)">
<i class="icon-download"></i>
</button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
qualityCalls: Array
});
defineEmits(['play-call', 'download-call']);
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.view-all-btn {
background: none;
border: 1px solid #ccc;
border-radius: 4px;
padding: 6px 12px;
cursor: pointer;
}
.quality-calls .call-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.quality-calls .call-item:last-child {
border-bottom: none;
}
.caller-name {
font-weight: bold;
}
.call-details {
font-size: 12px;
color: #666;
}
.call-actions button {
background: none;
border: none;
cursor: pointer;
font-size: 18px;
margin-left: 10px;
}
.icon-play::before { content: '▶'; }
.icon-download::before { content: '⬇'; }
</style>

View File

@@ -0,0 +1,116 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>业绩排行榜</h3>
<select v-model="rankingPeriod" class="period-select">
<option value="month">本月</option>
<option value="quarter">本季度</option>
<option value="year">本年</option>
</select>
</div>
<div class="ranking-list">
<div v-for="(item, index) in rankingData.slice(0, 4)" :key="item.id" class="ranking-item">
<div class="rank-number" :class="getRankClass(index)">
{{ index + 1 }}
</div>
<div class="employee-info">
<div class="employee-name">{{ item.name }}</div>
<div class="employee-dept">{{ item.department }}</div>
</div>
<div class="performance-value">
¥{{ formatNumber(item.performance) }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, ref } from 'vue';
defineProps({
rankingData: Array,
formatNumber: Function,
getRankClass: Function
});
const rankingPeriod = ref('month');
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.period-select {
border: 1px solid #ccc;
border-radius: 4px;
padding: 6px 10px;
}
.ranking-list .ranking-item {
display: flex;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.ranking-list .ranking-item:last-child {
border-bottom: none;
}
.rank-number {
font-size: 16px;
font-weight: bold;
width: 30px;
text-align: center;
margin-right: 15px;
}
.rank-number.gold {
color: #FFD700;
}
.rank-number.silver {
color: #C0C0C0;
}
.rank-number.bronze {
color: #CD7F32;
}
.employee-info {
flex-grow: 1;
}
.employee-name {
font-weight: bold;
}
.employee-dept {
font-size: 12px;
color: #666;
}
.performance-value {
font-weight: bold;
}
</style>

View File

@@ -0,0 +1,92 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>销售实时进度</h3>
<span class="metric-period">今日</span>
</div>
<div class="sales-progress-tips">
<div class="tip-item success">
<i class="icon-check-circle"></i>
<span>{{ salesData.successTip }}</span>
</div>
<div class="tip-item warning">
<i class="icon-alert-circle"></i>
<span>{{ salesData.warningTip }}</span>
</div>
<div class="tip-item info">
<i class="icon-info-circle"></i>
<span>{{ salesData.infoTip }}</span>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps } from 'vue';
defineProps({
salesData: Object
});
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.metric-period {
font-size: 14px;
color: #666;
}
.sales-progress-tips {
display: flex;
flex-direction: column;
gap: 10px;
}
.tip-item {
display: flex;
align-items: center;
font-size: 14px;
}
.tip-item i {
margin-right: 8px;
font-size: 18px;
}
.tip-item.success {
color: #4CAF50;
}
.tip-item.warning {
color: #FF9800;
}
.tip-item.info {
color: #2196F3;
}
.icon-check-circle::before { content: '✔'; }
.icon-alert-circle::before { content: '⚠'; }
.icon-info-circle::before { content: ''; }
</style>

View File

@@ -0,0 +1,120 @@
<template>
<div class="dashboard-card">
<div class="card-header">
<h3>下发任务</h3>
<button class="add-task-btn" @click="$emit('show-task-modal')">
<i class="icon-plus"></i> 新建任务
</button>
</div>
<div class="task-list compact">
<div v-for="task in tasks.slice(0, 3)" :key="task.id" class="task-item">
<div class="task-content">
<div class="task-title">{{ task.title }}</div>
<div class="task-meta" style="display: flex; gap: 15px;">
<span class="assignee">分配给: {{ task.assignee }}</span>
<span class="deadline">截止: {{ formatDate(task.deadline) }}</span>
</div>
</div>
<div class="task-status" :class="task.status">
{{ getTaskStatusText(task.status) }}
</div>
</div>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from 'vue';
defineProps({
tasks: Array,
formatDate: Function,
getTaskStatusText: Function
});
defineEmits(['show-task-modal']);
</script>
<style scoped>
.dashboard-card {
background-color: #fff;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
padding: 20px;
display: flex;
flex-direction: column;
flex: 1;
}
.card-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.card-header h3 {
margin: 0;
font-size: 18px;
}
.add-task-btn {
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
padding: 8px 12px;
cursor: pointer;
display: flex;
align-items: center;
}
.add-task-btn i {
margin-right: 4px;
}
.task-list.compact .task-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
border-bottom: 1px solid #eee;
}
.task-list.compact .task-item:last-child {
border-bottom: none;
}
.task-title {
font-weight: bold;
}
.task-meta {
font-size: 12px;
color: #666;
}
.task-status {
padding: 4px 8px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.task-status.pending {
background-color: #FFC107;
color: #fff;
}
.task-status.completed {
background-color: #4CAF50;
color: #fff;
}
.task-status.overdue {
background-color: #F44336;
color: #fff;
}
.icon-plus::before { content: '+'; }
</style>

File diff suppressed because it is too large Load Diff