feat(销售时间轴): 添加课程1-4子时间轴并优化健康度显示

添加课程1-4的详细子时间轴组件,展示各课程阶段的转化情况
重构健康度显示逻辑,使用新的CSS类名系统
移除按钮中的SVG图标,调整按钮字体大小
This commit is contained in:
2025-08-30 16:25:07 +08:00
parent beec8c6cfb
commit d204c7befe
2 changed files with 362 additions and 25 deletions

View File

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