feat(销售时间轴): 添加课程1-4子时间轴并优化健康度显示
添加课程1-4的详细子时间轴组件,展示各课程阶段的转化情况 重构健康度显示逻辑,使用新的CSS类名系统 移除按钮中的SVG图标,调整按钮字体大小
This commit is contained in:
@@ -36,6 +36,167 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课1-4子时间轴 -->
|
||||
<div v-if="selectedStage === '课1-4'" class="course-sub-timeline">
|
||||
<div class="sub-timeline-container">
|
||||
<!-- 课1子时间轴 -->
|
||||
<div class="course-timeline">
|
||||
<div class="course-header">
|
||||
<span class="course-title">课1</span>
|
||||
<span class="course-conversion">转化率: {{ getCourseConversionRate(1) }}%</span>
|
||||
</div>
|
||||
<div class="mini-timeline">
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(1, '课1') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">课1</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(1, '课1') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(1, '付定金') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">付定金</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(1, '付定金') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课2子时间轴 -->
|
||||
<div class="course-timeline">
|
||||
<div class="course-header">
|
||||
<span class="course-title">课2</span>
|
||||
</div>
|
||||
<div class="mini-timeline">
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '课2') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">课2</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(2, '课2') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '点击未支付') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">点击支付</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(2, '点击未支付') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '付定金') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">付定金</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(2, '付定金') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '定金转化') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">定金转化</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(2, '定金转化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(2, '成交') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">成交</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(2, '成交') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课3子时间轴 -->
|
||||
<div class="course-timeline">
|
||||
<div class="course-header">
|
||||
<span class="course-title">课3</span>
|
||||
</div>
|
||||
<div class="mini-timeline">
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '课3') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">课3</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(3, '课3') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '点击未支付') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">点击未支付</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(3, '点击未支付') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '付定金') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">付定金</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(3, '付定金') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '定金转化') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">定金转化</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(3, '定金转化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(3, '成交') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">成交</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(3, '成交') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 课4子时间轴 -->
|
||||
<div class="course-timeline">
|
||||
<div class="course-header">
|
||||
<span class="course-title">课4</span>
|
||||
</div>
|
||||
<div class="mini-timeline">
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '课4') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">课4</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(4, '课4') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '点击未支付') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">点击未付</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(4, '点击未支付') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '付定金') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">付定金</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(4, '付定金') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '定金转化') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">定金转化</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(4, '定金转化') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mini-stage" :class="{ active: getCourseStageCount(4, '成交') > 0 }">
|
||||
<div class="mini-marker"></div>
|
||||
<div class="mini-content">
|
||||
<span class="mini-title">成交</span>
|
||||
<span class="mini-count">{{ getCourseStageCount(4, '成交') }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="task-body">
|
||||
<div class="actionable-list">
|
||||
<div class="items-grid">
|
||||
@@ -44,7 +205,7 @@
|
||||
:key="item.id"
|
||||
:id="`contact-item-${item.id}`"
|
||||
class="action-item"
|
||||
:class="[getHealthIndicator(item.health).class, { 'active': item.id === selectedContactId }]"
|
||||
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
|
||||
@click="selectContact(item.id)">
|
||||
<div class="item-content">
|
||||
<!-- 头像区域 -->
|
||||
@@ -69,7 +230,7 @@
|
||||
<span class="course-attendance">
|
||||
{{ getAttendedLessons(item.class_situation,item.class_num) }}
|
||||
</span>
|
||||
<span class="course-type">{{ item.type }}</span>
|
||||
<span class="course-type" v-if="item.type">{{ item.type }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -380,7 +541,7 @@ const stages = computed(() => {
|
||||
{ id: 4, name: '待联系', displayName: '待联系', count: getStageCount('待联系'), color: '#bbdefb' },
|
||||
{ id: 5, name: '待到课', displayName: '待到课', count: getStageCount('待到课'), color: '#bbdefb'},
|
||||
{ id: 6, name: '课1-4', displayName: '课1-4', count: getStageCount('课1-4'), color: '#81c784' },
|
||||
{ id: 7, name: '点击未支付', displayName: '点击未支付', count: getStageCount('点击未支付'), color: '#42a5f5' },
|
||||
{ id: 7, name: '点击支付', displayName: '点击支付', count: getStageCount('点击未支付'), color: '#42a5f5' },
|
||||
{ id: 8, name: '付定金', displayName: '付定金', count: getStageCount('付定金'), color: '#2196f3' },
|
||||
{ id: 9, name: '定金转化', displayName: '定金转化', count: getStageCount('定金转化'), color: '#1e88e5' },
|
||||
{ id: 10, name: '成交', displayName: '成交', count: getStageCount('成交'), color: '#1976d2' }
|
||||
@@ -505,12 +666,6 @@ const selectContact = (id) => {
|
||||
emit('select-contact', id);
|
||||
};
|
||||
|
||||
const getHealthIndicator = (score) => {
|
||||
if (score > 80) return { class: 'health-good', text: '健康', textColor: 'text-green' };
|
||||
if (score > 50) return { class: 'health-ok', text: '一般', textColor: 'text-amber' };
|
||||
return { class: 'health-risk', text: '高风险', textColor: 'text-red' };
|
||||
};
|
||||
|
||||
// 显示发言内容弹框
|
||||
const showSpeakMessages = (speakMessages) => {
|
||||
modalMessages.value = speakMessages || [];
|
||||
@@ -522,6 +677,55 @@ const closeModal = () => {
|
||||
modalMessages.value = [];
|
||||
};
|
||||
|
||||
// 获取课程阶段数量
|
||||
const getCourseStageCount = (courseNumber, stageType) => {
|
||||
if (!props.customersList && !props.courseCustomers) return 0;
|
||||
|
||||
// 根据课程号和阶段类型筛选客户
|
||||
let filteredCustomers = [];
|
||||
|
||||
if (stageType === `课${courseNumber}`) {
|
||||
// 课程阶段:筛选有对应课程到课记录的客户
|
||||
filteredCustomers = props.customersList.filter(customer => {
|
||||
const classNum = customer.class_num;
|
||||
const classSituation = customer.class_situation;
|
||||
|
||||
// 检查class_num字段
|
||||
if (classNum && Array.isArray(classNum)) {
|
||||
return classNum.includes(courseNumber);
|
||||
}
|
||||
|
||||
// 检查class_situation字段
|
||||
if (classSituation) {
|
||||
if (Array.isArray(classSituation)) {
|
||||
return classSituation.includes(courseNumber);
|
||||
}
|
||||
if (typeof classSituation === 'object') {
|
||||
return classSituation.hasOwnProperty(courseNumber.toString());
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
} else {
|
||||
// 其他阶段:从courseCustomers中筛选
|
||||
const courseKey = `课${courseNumber}`;
|
||||
if (props.courseCustomers?.[courseKey]) {
|
||||
filteredCustomers = props.courseCustomers[courseKey].filter(customer => customer.type === stageType);
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCustomers.length;
|
||||
};
|
||||
|
||||
// 获取课程转化率
|
||||
const getCourseConversionRate = (courseNumber) => {
|
||||
const courseStageCount = getCourseStageCount(courseNumber, `课${courseNumber}`);
|
||||
const depositCount = getCourseStageCount(courseNumber, '付定金');
|
||||
|
||||
if (courseStageCount === 0) return 0;
|
||||
return Math.round((depositCount / courseStageCount) * 100);
|
||||
};
|
||||
|
||||
const getAttendedLessons = (classSituation, classNum) => {
|
||||
// 优先使用 class_num 字段
|
||||
@@ -547,6 +751,23 @@ const getAttendedLessons = (classSituation, classNum) => {
|
||||
|
||||
return '未到课';
|
||||
};
|
||||
|
||||
// 获取健康度对应的CSS类
|
||||
const getHealthClass = (health) => {
|
||||
if (!health && health !== 0) return '';
|
||||
|
||||
const healthValue = parseFloat(health);
|
||||
|
||||
if (healthValue >= 80) {
|
||||
return 'health-good';
|
||||
} else if (healthValue >= 60) {
|
||||
return 'health-normal';
|
||||
} else if (healthValue >= 40) {
|
||||
return 'health-warning';
|
||||
} else {
|
||||
return 'health-danger';
|
||||
}
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -611,7 +832,6 @@ $indigo: #4f46e5;
|
||||
background: $slate-50;
|
||||
border-radius: 0.5rem;
|
||||
padding: 0.75rem;
|
||||
border-left: 3px solid $primary;
|
||||
|
||||
.lesson-header {
|
||||
margin-bottom: 0.5rem;
|
||||
@@ -953,7 +1173,6 @@ $indigo: #4f46e5;
|
||||
padding: 0.25rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid $slate-200;
|
||||
border-left-width: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s ease-in-out;
|
||||
min-height: 50px;
|
||||
@@ -1221,10 +1440,8 @@ $indigo: #4f46e5;
|
||||
}
|
||||
}
|
||||
|
||||
// Health Status Colors
|
||||
.health-good { border-color: $success; }
|
||||
.health-ok { border-color: $warning; }
|
||||
.health-risk { border-color: $danger; }
|
||||
|
||||
// Health status styles
|
||||
|
||||
// Clickable styles
|
||||
.clickable {
|
||||
@@ -1340,4 +1557,133 @@ $indigo: #4f46e5;
|
||||
color: #374151;
|
||||
}
|
||||
|
||||
/* 课1-4子时间轴样式 */
|
||||
.course-sub-timeline {
|
||||
margin: 16px 0;
|
||||
padding: 12px;
|
||||
background-color: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
|
||||
.sub-timeline-container {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
grid-template-rows: 1fr 1fr;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.course-timeline {
|
||||
background: white;
|
||||
border-radius: 6px;
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #e5e7eb;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.course-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
padding-bottom: 4px;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.course-title {
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.course-conversion {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
background-color: #f1f5f9;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.mini-timeline {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 4px;
|
||||
position: relative;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.mini-timeline::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
left: 6px;
|
||||
right: 6px;
|
||||
height: 2px;
|
||||
background-color: #e2e8f0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.mini-stage {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.mini-marker {
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
border-radius: 50%;
|
||||
background-color: #e2e8f0;
|
||||
border: 2px solid white;
|
||||
transition: all 0.3s ease;
|
||||
margin-bottom: 4px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.mini-stage.active .mini-marker {
|
||||
background-color: #3b82f6;
|
||||
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.mini-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-title {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
margin-bottom: 2px;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.mini-stage.active .mini-title {
|
||||
color: #1e293b;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mini-count {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
background-color: #f1f5f9;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
min-width: 16px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.mini-stage.active .mini-count {
|
||||
background-color: #dbeafe;
|
||||
color: #1d4ed8;
|
||||
}
|
||||
|
||||
</style>
|
||||
Reference in New Issue
Block a user