feat: 初始化Vue项目并添加核心功能模块
添加了Vue3项目基础结构,包括路由、状态管理和API配置 实现了销售管理系统的核心功能模块,包括业绩看板、团队管理和客户分析 集成了Element Plus UI组件库和ECharts数据可视化 添加了全局样式和响应式布局支持
This commit is contained in:
252
my-vue-app/src/views/topOne/components/CampManagement.vue
Normal file
252
my-vue-app/src/views/topOne/components/CampManagement.vue
Normal 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>
|
||||
104
my-vue-app/src/views/topOne/components/CommunicationData.vue
Normal file
104
my-vue-app/src/views/topOne/components/CommunicationData.vue
Normal 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>
|
||||
119
my-vue-app/src/views/topOne/components/CustomerProfile.vue
Normal file
119
my-vue-app/src/views/topOne/components/CustomerProfile.vue
Normal 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>
|
||||
93
my-vue-app/src/views/topOne/components/DataDetail.vue
Normal file
93
my-vue-app/src/views/topOne/components/DataDetail.vue
Normal 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>
|
||||
233
my-vue-app/src/views/topOne/components/DataTable.vue
Normal file
233
my-vue-app/src/views/topOne/components/DataTable.vue
Normal 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>
|
||||
80
my-vue-app/src/views/topOne/components/FunnelChart.vue
Normal file
80
my-vue-app/src/views/topOne/components/FunnelChart.vue
Normal 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>
|
||||
107
my-vue-app/src/views/topOne/components/KpiMetrics.vue
Normal file
107
my-vue-app/src/views/topOne/components/KpiMetrics.vue
Normal 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>
|
||||
101
my-vue-app/src/views/topOne/components/QualityCalls.vue
Normal file
101
my-vue-app/src/views/topOne/components/QualityCalls.vue
Normal 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>
|
||||
116
my-vue-app/src/views/topOne/components/RankingList.vue
Normal file
116
my-vue-app/src/views/topOne/components/RankingList.vue
Normal 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>
|
||||
92
my-vue-app/src/views/topOne/components/SalesProgress.vue
Normal file
92
my-vue-app/src/views/topOne/components/SalesProgress.vue
Normal 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>
|
||||
120
my-vue-app/src/views/topOne/components/TaskList.vue
Normal file
120
my-vue-app/src/views/topOne/components/TaskList.vue
Normal 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>
|
||||
1181
my-vue-app/src/views/topOne/components/secondTop.vue
Normal file
1181
my-vue-app/src/views/topOne/components/secondTop.vue
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user