- 在客户数据中添加time_and_camp_stage字段用于记录付款行为发生的课程阶段 - 重构课程阶段筛选逻辑,优先使用time_and_camp_stage字段进行精确匹配 - 移除已注释的旧代码 - 更新API基础路径为生产环境
1699 lines
47 KiB
Vue
1699 lines
47 KiB
Vue
<template>
|
||
<div class="sales-timeline-with-task-list">
|
||
<div class="timeline-title-section" style="display: flex; align-items: center; gap: 10px;">
|
||
<h2 style="font-size: 18px;margin: 0;">销售时间线</h2>
|
||
<span style="font-size: 14px;">客户转化全流程跟踪</span>
|
||
</div>
|
||
|
||
<div class="sales-timeline">
|
||
<div class="timeline-container">
|
||
<div class="timeline-line"></div>
|
||
<div
|
||
v-for="(stage, index) in stages"
|
||
:key="stage.id"
|
||
class="timeline-stage"
|
||
:class="{ 'active': stage.count > 0, 'selected': selectedStage === stage.name }"
|
||
@click="selectStage(stage.name)"
|
||
>
|
||
<div class="stage-marker">
|
||
<div class="marker-circle">
|
||
<span class="stage-number">{{ index + 1 }}</span>
|
||
</div>
|
||
<div class="marker-line" v-if="index < stages.length - 1"></div>
|
||
</div>
|
||
<div class="stage-content">
|
||
<h3 class="stage-title">{{ stage.displayName || stage.name }}</h3>
|
||
<div class="stage-stats">
|
||
<span class="stage-count">{{ stage.name === '全部' ? customersCount : stage.count }}</span>
|
||
<span class="stage-label">位</span>
|
||
</div>
|
||
<div class="stage-unName">
|
||
<span class="stage-unName" v-if="stage.unName">{{ stage.unName }}</span>
|
||
</div>
|
||
<div v-if="stage.name !== '全部'" class="stage-percentage">{{ getPercentage(stage.count, stage.id, stage.name) }}%</div>
|
||
</div>
|
||
</div>
|
||
</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 }" @click="selectCourseStage(1, '课1')">
|
||
<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 }" @click="selectCourseStage(1, '付定金')">
|
||
<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 }" @click="selectCourseStage(2, '课2')">
|
||
<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 }" @click="selectCourseStage(2, '点击未支付')">
|
||
<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 }" @click="selectCourseStage(2, '付定金')">
|
||
<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 }" @click="selectCourseStage(2, '定金转化')">
|
||
<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 }" @click="selectCourseStage(2, '成交')">
|
||
<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 }" @click="selectCourseStage(3, '课3')">
|
||
<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 }" @click="selectCourseStage(3, '点击未支付')">
|
||
<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 }" @click="selectCourseStage(3, '付定金')">
|
||
<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 }" @click="selectCourseStage(3, '定金转化')">
|
||
<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 }" @click="selectCourseStage(3, '成交')">
|
||
<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 }" @click="selectCourseStage(4, '课4')">
|
||
<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 }" @click="selectCourseStage(4, '点击未支付')">
|
||
<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 }" @click="selectCourseStage(4, '付定金')">
|
||
<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 }" @click="selectCourseStage(4, '定金转化')">
|
||
<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 }" @click="selectCourseStage(4, '成交')">
|
||
<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">
|
||
<div
|
||
v-for="item in contacts"
|
||
:key="item.id"
|
||
:id="`contact-item-${item.id}`"
|
||
class="action-item"
|
||
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
|
||
@click="selectContact(item.id)">
|
||
<div class="item-content">
|
||
<!-- 头像区域 -->
|
||
<div class="avatar-section">
|
||
<div class="avatar">
|
||
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 信息区域 -->
|
||
<div class="info-section">
|
||
<div class="item-header">
|
||
<div class="item-name-group">
|
||
<span class="item-name" :title="item.name">{{ item.name }}</span>
|
||
</div>
|
||
<span class="item-time">{{ item.time }}</span>
|
||
</div>
|
||
<div class="item-footer">
|
||
<div class="profession-education">
|
||
<span class="profession">{{ item.profession||'未知' }}</span>
|
||
<span class="education">{{ item.education||'未知' }}</span>
|
||
<span class="course-attendance">
|
||
{{ getAttendedLessons(item.class_situation,item.class_num) }}
|
||
</span>
|
||
<span class="course-type" v-if="item.type">{{ item.type }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 到课详情区域 -->
|
||
<div v-if="selectedContactId && selectedContactDetails && (['课1-4', '课1', '课2', '课3', '课4'].includes(selectedStage))" class="course-details">
|
||
<div class="course-details-header">
|
||
<h4>{{ selectedContactDetails.name }} - 到课详情</h4>
|
||
</div>
|
||
<div class="course-details-content">
|
||
<div v-if="!selectedContactDetails.class_situation || Object.keys(selectedContactDetails.class_situation).length === 0" class="no-data">
|
||
未到课
|
||
</div>
|
||
<div v-else class="course-lessons">
|
||
<div
|
||
v-for="(lessonData, lessonNum) in selectedContactDetails.class_situation"
|
||
:key="lessonNum"
|
||
class="lesson-item"
|
||
>
|
||
<div class="lesson-header">
|
||
<span class="lesson-number">第{{ lessonNum }}节课</span>
|
||
</div>
|
||
<div class="lesson-details">
|
||
<div class="detail-item">
|
||
<span class="detail-label">听课时长:</span>
|
||
<span class="detail-value">{{ Math.round((lessonData.live_maximum_length_time || 0) / 60) }}分钟</span>
|
||
</div>
|
||
<div class="detail-item">
|
||
<span class="detail-label">是否看回放:</span>
|
||
<span class="detail-value">{{ lessonData.playback_maximum_length_time ? '是' : '否' }}</span>
|
||
</div>
|
||
<div class="detail-item" v-if="lessonData.playback_maximum_length_time">
|
||
<span class="detail-label">回放时长:</span>
|
||
<span class="detail-value">{{ Math.round((lessonData.playback_maximum_length_time || 0) / 60) }}分钟</span>
|
||
</div>
|
||
<div class="detail-item" v-if="lessonData.speak_message">
|
||
<span class="detail-label">直播发言:</span>
|
||
<span class="detail-value clickable" @click="showSpeakMessages(lessonData.speak_message)">{{ lessonData.speak_message.length || 0 }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 发言内容弹窗 -->
|
||
<div v-if="showModal" class="modal-overlay" @click="closeModal">
|
||
<div class="modal-content" @click.stop>
|
||
<div class="modal-header">
|
||
<h3>直播发言内容</h3>
|
||
<button class="close-btn" @click="closeModal">×</button>
|
||
</div>
|
||
<div class="modal-body">
|
||
<div v-if="!modalMessages || modalMessages.length === 0" class="no-messages">
|
||
暂无发言内容
|
||
</div>
|
||
<div v-else class="messages-list">
|
||
<div v-for="(message, index) in modalMessages" :key="index" class="message-item">
|
||
<span class="message-number">{{ index + 1 }}.</span>
|
||
<span class="message-content">{{ message }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed, ref } from 'vue';
|
||
|
||
// 弹窗相关的响应式数据
|
||
const showModal = ref(false);
|
||
const modalMessages = ref([]);
|
||
|
||
// 定义props
|
||
const props = defineProps({
|
||
data: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
selectedStage: {
|
||
type: String,
|
||
default: 'all'
|
||
},
|
||
contacts: {
|
||
type: Array,
|
||
required: true
|
||
},
|
||
selectedContactId: {
|
||
type: Number,
|
||
default: null
|
||
},
|
||
courseCustomers: {
|
||
type: Object,
|
||
default: () => ({})
|
||
},
|
||
customersList: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
customersCount: {
|
||
type: Number,
|
||
default: 0
|
||
},
|
||
payMoneyCustomersList: {
|
||
type: Array,
|
||
default: () => []
|
||
},
|
||
payMoneyCustomersCount: {
|
||
type: Number,
|
||
default: 0
|
||
}
|
||
});
|
||
|
||
const emit = defineEmits(['stage-select', 'select-contact']);
|
||
|
||
const totalCustomers = computed(() => {
|
||
// 全部阶段的数量就是前6个阶段的数量
|
||
if (props.customersList?.length > 0) return props.customersList.length;
|
||
|
||
// 如果没有实际数据,使用props.data中的数据作为后备
|
||
if (props.data['全部']) return props.data['全部'];
|
||
|
||
const baseStages = [
|
||
props.data['未加微'] || 0,
|
||
props.data['已加微'] || 0,
|
||
props.data['已入群'] || 0,
|
||
props.data['已填表单'] || 0,
|
||
props.data['课1-4'] || 0,
|
||
props.data['点击未支付'] || 0,
|
||
props.data['付定金'] || 0,
|
||
props.data['定价转化'] || 0,
|
||
props.data['成交'] || 0
|
||
];
|
||
return Math.max(...baseStages, 1);
|
||
});
|
||
|
||
const getStageCount = (stageType) => {
|
||
// 成交阶段从payMoneyCustomersList获取数量
|
||
if (stageType === '成交') {
|
||
return props.payMoneyCustomersCount || (props.payMoneyCustomersList?.length || 0);
|
||
}
|
||
|
||
// 课1-4阶段从courseCustomers获取数量
|
||
if (stageType === '课1-4' && props.courseCustomers?.['课1-4']) {
|
||
return props.courseCustomers['课1-4'].length;
|
||
}
|
||
|
||
// 后3个阶段从courseCustomers中筛选
|
||
if (['点击未支付', '付定金', '定金转化'].includes(stageType)) {
|
||
if (props.courseCustomers?.['课1-4']) {
|
||
return props.courseCustomers['课1-4'].filter(customer => customer.type === stageType).length;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// 待填表单阶段特殊处理:筛选customer_occupation或customer_child_education字段为'未知'的客户
|
||
if (stageType === '待填表单') {
|
||
if (props.customersList?.length) {
|
||
return props.customersList.filter(customer => {
|
||
return (customer.customer_occupation === '未知' || !customer.customer_occupation) ||
|
||
(customer.customer_child_education === '未知' || !customer.customer_child_education);
|
||
}).length;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// 待到课阶段特殊处理:筛选没有到课数据的客户
|
||
if (stageType === '待到课') {
|
||
if (props.customersList?.length) {
|
||
return props.customersList.filter(customer => {
|
||
const classNum = customer.class_num;
|
||
const classSituation = customer.class_situation;
|
||
|
||
// 优先检查class_num字段
|
||
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
|
||
return false; // 有class_num数据,不是待到课
|
||
}
|
||
|
||
// 检查class_situation字段
|
||
if (!classSituation) return true; // 没有class_situation,是待到课
|
||
|
||
if (Array.isArray(classSituation)) {
|
||
return classSituation.length === 0; // 空数组,是待到课
|
||
}
|
||
|
||
if (typeof classSituation === 'object') {
|
||
const lessonNumbers = Object.keys(classSituation)
|
||
.map(key => parseInt(key))
|
||
.filter(num => !isNaN(num));
|
||
return lessonNumbers.length === 0; // 没有有效的课程数据,是待到课
|
||
}
|
||
|
||
return true; // 其他情况默认为待到课
|
||
}).length;
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// 前6个阶段从customersList中筛选
|
||
if (props.customersList?.length) {
|
||
return props.customersList.filter(customer => customer.type === stageType).length;
|
||
}
|
||
|
||
// 如果没有数据,使用props.data中的数据
|
||
return props.data[stageType] || 0;
|
||
};
|
||
|
||
const stages = computed(() => {
|
||
// 全部阶段的数量就是前6个阶段的数量
|
||
const totalCount = props.customersList?.length || 0;
|
||
|
||
const stageList = [
|
||
{ id: 0, name: '全部', displayName: '全部', count: totalCount, color: '#f3f4f6' },
|
||
{ id: 1, name: '待加微', displayName: '待加微', count: getStageCount('待加微'), color: '#e3f2fd' },
|
||
{ id: 2, name: '待填表单', displayName: '待填表单', count: getStageCount('待填表单'), color: '#90caf9' },
|
||
{ id: 3, name: '待入群', displayName: '待入群', count: getStageCount('待入群'), color: '#bbdefb' },
|
||
{ 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: 8, name: '付定金', displayName: '付定金', count: getStageCount('付定金'), color: '#2196f3' },
|
||
{ id: 9, name: '定金转化', displayName: '定金转化', count: getStageCount('定金转化'), color: '#1e88e5' },
|
||
{ id: 10, name: '成交', displayName: '成交', count: getStageCount('成交'), color: '#1976d2' }
|
||
];
|
||
|
||
return stageList;
|
||
});
|
||
|
||
|
||
const getPercentage = (count, stageId, stageName) => {
|
||
if (totalCustomers.value === 0) return 0;
|
||
|
||
// 待填表单阶段特殊计算:(总数-有数据人数)/总数
|
||
if (stageName === '待填表单') {
|
||
return Math.round((totalCustomers.value - count) / totalCustomers.value * 100);
|
||
}
|
||
|
||
// 待入群阶段特殊计算:(总数-待加微人数-待入群人数)/总数
|
||
if (stageName === '待入群') {
|
||
const waitAddWechatCount = getStageCount('待加微');
|
||
const waitJoinGroupCount = getStageCount('待入群');
|
||
return Math.round((totalCustomers.value - waitAddWechatCount - waitJoinGroupCount) / totalCustomers.value * 100);
|
||
}
|
||
|
||
// 待联系阶段特殊计算:(总数-待加微人数-待联系人数)/总数
|
||
if (stageName === '待联系') {
|
||
const waitAddWechatCount = getStageCount('待加微');
|
||
const waitContactCount = getStageCount('待联系');
|
||
return Math.round((totalCustomers.value - waitAddWechatCount - waitContactCount) / totalCustomers.value * 100);
|
||
}
|
||
|
||
// 待到课阶段特殊计算:(总数-有到课数据人数)/总数
|
||
if (stageName === '待到课') {
|
||
return Math.round((totalCustomers.value - count) / totalCustomers.value * 100);
|
||
}
|
||
|
||
// 阶段1-5使用现在的算法(转化率)
|
||
if (stageId >= 1 && stageId <= 5) {
|
||
return Math.round((totalCustomers.value - count) / totalCustomers.value * 100);
|
||
}
|
||
|
||
// 阶段5-13使用直接数量除以总数
|
||
if (stageId >5 && stageId <= 13) {
|
||
return Math.round((count / totalCustomers.value) * 100);
|
||
}
|
||
|
||
// 其他阶段保持原有逻辑
|
||
return Math.round((totalCustomers.value - count) / totalCustomers.value * 100);
|
||
};
|
||
|
||
|
||
const selectStage = (stageName) => {
|
||
let filteredCustomers = [];
|
||
|
||
if (stageName === '全部') {
|
||
// 全部阶段只显示前6个阶段的客户数据
|
||
filteredCustomers = props.customersList || [];
|
||
} else if (stageName === '课1-4' && props.courseCustomers?.['课1-4']) {
|
||
emit('stage-select', stageName, {
|
||
isCourseStage: true,
|
||
courseData: props.courseCustomers['课1-4'],
|
||
filteredCustomers: props.courseCustomers['课1-4']
|
||
});
|
||
return;
|
||
|
||
} else if (stageName === '成交') {
|
||
// 成交阶段使用payMoneyCustomersList数据
|
||
if (props.payMoneyCustomersList && props.payMoneyCustomersList.length > 0) {
|
||
filteredCustomers = props.payMoneyCustomersList.map(customer => ({
|
||
customer_name: customer.customer_name,
|
||
phone: customer.phone,
|
||
customer_occupation: customer.customer_occupation,
|
||
customer_child_education: customer.customer_child_education,
|
||
latest_message_time: customer.latest_message_time,
|
||
customer_avatar_url: customer.customer_avatar_url,
|
||
type: '成交'
|
||
}));
|
||
}
|
||
} else if (['点击未支付', '付定金', '定金转化'].includes(stageName)) {
|
||
// 后3个阶段从courseCustomers中筛选
|
||
if (props.courseCustomers?.['课1-4']) {
|
||
filteredCustomers = props.courseCustomers['课1-4'].filter(customer => customer.type === stageName);
|
||
}
|
||
} else if (stageName === '待填表单') {
|
||
// 待填表单阶段:筛选customer_occupation或customer_child_education字段为'未知'的客户
|
||
filteredCustomers = props.customersList.filter(customer => {
|
||
return (customer.customer_occupation === '未知' || !customer.customer_occupation) ||
|
||
(customer.customer_child_education === '未知' || !customer.customer_child_education);
|
||
});
|
||
} else if (stageName === '待到课') {
|
||
// 待到课阶段:筛选没有到课数据的客户
|
||
filteredCustomers = props.customersList.filter(customer => {
|
||
// 根据getAttendedLessons函数的逻辑判断是否为'未到课'
|
||
const classNum = customer.class_num;
|
||
const classSituation = customer.class_situation;
|
||
|
||
// 优先检查class_num字段
|
||
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
|
||
return false; // 有class_num数据,不是待到课
|
||
}
|
||
|
||
// 检查class_situation字段
|
||
if (!classSituation) return true; // 没有class_situation,是待到课
|
||
|
||
if (Array.isArray(classSituation)) {
|
||
return classSituation.length === 0; // 空数组,是待到课
|
||
}
|
||
|
||
if (typeof classSituation === 'object') {
|
||
const lessonNumbers = Object.keys(classSituation)
|
||
.map(key => parseInt(key))
|
||
.filter(num => !isNaN(num));
|
||
return lessonNumbers.length === 0; // 没有有效的课程数据,是待到课
|
||
}
|
||
|
||
return true; // 其他情况默认为待到课
|
||
});
|
||
} else {
|
||
// 前6个阶段从customersList中筛选
|
||
filteredCustomers = props.customersList.filter(customer => customer.type === stageName);
|
||
}
|
||
|
||
emit('stage-select', stageName, {
|
||
filteredCustomers,
|
||
stageType: stageName,
|
||
customerCount: filteredCustomers.length
|
||
});
|
||
};
|
||
|
||
const selectedContactDetails = computed(() => {
|
||
if (!props.selectedContactId) return null;
|
||
return props.contacts.find(contact => contact.id === props.selectedContactId);
|
||
});
|
||
|
||
const selectContact = (id) => {
|
||
emit('select-contact', id);
|
||
};
|
||
|
||
// 显示发言内容弹框
|
||
const showSpeakMessages = (speakMessages) => {
|
||
modalMessages.value = speakMessages || [];
|
||
showModal.value = true;
|
||
};
|
||
|
||
const closeModal = () => {
|
||
showModal.value = false;
|
||
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['课1-4']中筛选
|
||
if (props.courseCustomers?.['课1-4']) {
|
||
filteredCustomers = props.courseCustomers['课1-4'].filter(customer => {
|
||
// 检查客户的付款行为是否发生在指定课程阶段并且类型匹配
|
||
let hasPaymentInCourse = false;
|
||
|
||
// 优先使用time_and_camp_stage字段判断付款行为发生的课程阶段
|
||
if (customer.time_and_camp_stage && Array.isArray(customer.time_and_camp_stage)) {
|
||
hasPaymentInCourse = customer.time_and_camp_stage.some(record => {
|
||
return record.camp_stage === `课${courseNumber}` && record.pay_status === stageType;
|
||
});
|
||
}
|
||
|
||
// 如果time_and_camp_stage没有匹配的记录,则回退到class_num字段
|
||
if (!hasPaymentInCourse) {
|
||
const hasAttendedCourse = customer.class_num && Array.isArray(customer.class_num) && customer.class_num.includes(courseNumber);
|
||
hasPaymentInCourse = hasAttendedCourse && customer.type === stageType;
|
||
}
|
||
|
||
return hasPaymentInCourse;
|
||
});
|
||
}
|
||
}
|
||
|
||
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 字段
|
||
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
|
||
const filteredClassNum = classNum.filter(num => num !== -1);
|
||
return filteredClassNum.length > 0 ? filteredClassNum.sort((a, b) => a - b).join(' ') : '未到课';
|
||
}
|
||
|
||
// 如果没有 class_num,则使用 class_situation
|
||
if (!classSituation) return '未到课';
|
||
|
||
if (Array.isArray(classSituation)) {
|
||
const filteredSituation = classSituation.filter(item => item !== -1);
|
||
return filteredSituation.length > 0 ? filteredSituation.join(' ') : '未到课';
|
||
}
|
||
if (typeof classSituation === 'object') {
|
||
const lessonNumbers = Object.keys(classSituation)
|
||
.map(key => parseInt(key))
|
||
.filter(num => !isNaN(num) && num !== -1)
|
||
.sort((a, b) => a - b);
|
||
return lessonNumbers.length > 0 ? lessonNumbers.join(' ') : '未到课';
|
||
}
|
||
|
||
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';
|
||
}
|
||
};
|
||
|
||
// 选择课程阶段
|
||
const selectCourseStage = (courseNumber, stageType) => {
|
||
let filteredCustomers = [];
|
||
|
||
if (stageType === `课${courseNumber}`) {
|
||
// 课程阶段:从customersList中筛选
|
||
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中筛选
|
||
if (props.courseCustomers?.['课1-4']) {
|
||
filteredCustomers = props.courseCustomers['课1-4'].filter(customer => {
|
||
// 检查客户的付款行为是否发生在指定课程阶段并且类型匹配
|
||
let hasPaymentInCourse = false;
|
||
|
||
// 优先使用time_and_camp_stage字段判断付款行为发生的课程阶段
|
||
if (customer.time_and_camp_stage && Array.isArray(customer.time_and_camp_stage)) {
|
||
hasPaymentInCourse = customer.time_and_camp_stage.some(record => {
|
||
return record.camp_stage === `课${courseNumber}` && record.pay_status === stageType;
|
||
});
|
||
}
|
||
|
||
// 如果time_and_camp_stage没有匹配的记录,则回退到class_num字段
|
||
if (!hasPaymentInCourse) {
|
||
const hasAttendedCourse = customer.class_num && customer.class_num.includes(courseNumber);
|
||
hasPaymentInCourse = hasAttendedCourse && customer.type === stageType;
|
||
}
|
||
|
||
return hasPaymentInCourse;
|
||
});
|
||
}
|
||
}
|
||
|
||
// 发送子时间轴选择事件给父组件,使用不同的事件名称避免与主轴冲突
|
||
emit('sub-stage-select', {
|
||
filteredCustomers,
|
||
stageType: `课${courseNumber}-${stageType}`,
|
||
customerCount: filteredCustomers.length,
|
||
courseNumber,
|
||
originalStageType: stageType,
|
||
keepSubTimeline: true // 标识保持子时间轴显示
|
||
});
|
||
};
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
// Color Variables
|
||
$slate-50: #f8fafc;
|
||
$slate-100: #f1f5f9;
|
||
$slate-200: #e2e8f0;
|
||
$slate-300: #cbd5e1;
|
||
$slate-400: #94a3b8;
|
||
$slate-500: #64748b;
|
||
$slate-600: #475569;
|
||
$slate-700: #334155;
|
||
$slate-800: #1e293b;
|
||
$white: #ffffff;
|
||
|
||
$primary: #3b82f6;
|
||
$success: #22c55e;
|
||
$warning: #f59e0b;
|
||
$danger: #ef4444;
|
||
$indigo: #4f46e5;
|
||
|
||
// 到课详情区域样式
|
||
.course-details {
|
||
background: $white;
|
||
border-radius: 0.75rem;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
margin-top: 1rem;
|
||
overflow: hidden;
|
||
|
||
.course-details-header {
|
||
background: linear-gradient(135deg, #f8fafc, #e2e8f0);
|
||
padding: 0.75rem 1rem;
|
||
border-bottom: 1px solid $slate-200;
|
||
|
||
h4 {
|
||
margin: 0;
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: $slate-700;
|
||
}
|
||
}
|
||
|
||
.course-details-content {
|
||
padding: 1rem;
|
||
max-height: 200px;
|
||
overflow-y: auto;
|
||
|
||
.no-data {
|
||
text-align: center;
|
||
color: $slate-500;
|
||
font-size: 0.875rem;
|
||
padding: 1rem 0;
|
||
}
|
||
|
||
.course-lessons {
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
gap: 0.75rem;
|
||
|
||
.lesson-item {
|
||
background: $slate-50;
|
||
border-radius: 0.5rem;
|
||
padding: 0.75rem;
|
||
|
||
.lesson-header {
|
||
margin-bottom: 0.5rem;
|
||
|
||
.lesson-number {
|
||
font-size: 0.875rem;
|
||
font-weight: 600;
|
||
color: $slate-700;
|
||
}
|
||
}
|
||
|
||
.lesson-details {
|
||
display: flex;
|
||
flex-wrap: wrap;
|
||
gap: 1rem;
|
||
|
||
.detail-item {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.25rem;
|
||
|
||
.detail-label {
|
||
font-size: 0.75rem;
|
||
color: $slate-500;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.detail-value {
|
||
font-size: 0.875rem;
|
||
color: $slate-700;
|
||
font-weight: 600;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
margin-top: 0.75rem;
|
||
|
||
.course-details-header {
|
||
padding: 0.625rem 0.75rem;
|
||
|
||
h4 {
|
||
font-size: 0.8125rem;
|
||
}
|
||
}
|
||
|
||
.course-details-content {
|
||
padding: 0.75rem;
|
||
max-height: 150px;
|
||
|
||
.course-lessons {
|
||
gap: 0.5rem;
|
||
|
||
.lesson-item {
|
||
padding: 0.625rem;
|
||
|
||
.lesson-details {
|
||
gap: 0.75rem;
|
||
|
||
.detail-item {
|
||
.detail-label {
|
||
font-size: 0.6875rem;
|
||
}
|
||
|
||
.detail-value {
|
||
font-size: 0.8125rem;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.sales-timeline-with-task-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 1rem;
|
||
}
|
||
|
||
// Sales Timeline Styles
|
||
.sales-timeline {
|
||
padding: 1.5rem;
|
||
background: $white;
|
||
border-radius: 1rem;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||
|
||
@media (max-width: 768px) {
|
||
padding: 1rem;
|
||
}
|
||
}
|
||
|
||
.timeline-container {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: row;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
overflow-x: auto;
|
||
padding: 2rem 1rem 1rem 1rem;
|
||
|
||
@media (max-width: 768px) {
|
||
padding: 1.5rem 0.5rem 0.5rem 0.5rem;
|
||
-webkit-overflow-scrolling: touch;
|
||
scrollbar-width: none;
|
||
-ms-overflow-style: none;
|
||
|
||
&::-webkit-scrollbar {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
padding: 1rem 0.25rem 0.25rem 0.25rem;
|
||
}
|
||
}
|
||
|
||
.timeline-line {
|
||
position: absolute;
|
||
top: 4rem;
|
||
left: 2rem;
|
||
right: 2rem;
|
||
height: 3px;
|
||
background: linear-gradient(to right, $primary, $success);
|
||
border-radius: 2px;
|
||
|
||
@media (max-width: 768px) {
|
||
top: 3rem;
|
||
left: 1.5rem;
|
||
right: 1.5rem;
|
||
}
|
||
}
|
||
|
||
.timeline-stage {
|
||
position: relative;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
flex: 1;
|
||
min-width: 0;
|
||
cursor: pointer;
|
||
transition: all 0.3s ease;
|
||
border-radius: 8px;
|
||
padding: 0.5rem 8px;
|
||
|
||
@media (max-width: 768px) {
|
||
gap: 0.75rem;
|
||
min-width: 120px;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
gap: 0.5rem;
|
||
min-width: 100px;
|
||
padding: 0.25rem 4px;
|
||
}
|
||
|
||
&:hover {
|
||
background-color: rgba(59, 130, 246, 0.05);
|
||
transform: translateY(-2px);
|
||
|
||
.marker-circle {
|
||
border-color: $primary;
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
|
||
&.selected {
|
||
background-color: rgba(59, 130, 246, 0.1);
|
||
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.2);
|
||
|
||
.marker-circle {
|
||
border-color: $primary;
|
||
background: $primary;
|
||
transform: scale(1.2);
|
||
|
||
.stage-number {
|
||
color: $white;
|
||
}
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
.marker-circle {
|
||
background: linear-gradient(135deg, $primary, $success);
|
||
color: $white;
|
||
transform: scale(1.1);
|
||
box-shadow: 0 4px 15px rgba(59, 130, 246, 0.3);
|
||
}
|
||
|
||
.stage-content {
|
||
.stage-title {
|
||
font-size: 1rem;
|
||
color: $slate-800;
|
||
font-weight: 500;
|
||
}
|
||
|
||
.stage-count {
|
||
color: $primary;
|
||
font-weight: 700;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.stage-marker {
|
||
position: relative;
|
||
z-index: 3;
|
||
|
||
.marker-circle {
|
||
width: 4rem;
|
||
height: 4rem;
|
||
border-radius: 50%;
|
||
background: $slate-200;
|
||
border: 3px solid $white;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
position: relative;
|
||
z-index: 3;
|
||
|
||
@media (max-width: 768px) {
|
||
width: 3rem;
|
||
height: 3rem;
|
||
}
|
||
|
||
.stage-number {
|
||
font-size: 1.125rem;
|
||
font-weight: 700;
|
||
color: $slate-600;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 1rem;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
.stage-content {
|
||
text-align: center;
|
||
width: 100%;
|
||
|
||
.stage-title {
|
||
font-size: 1rem;
|
||
font-weight: 500;
|
||
color: $slate-700;
|
||
margin: 0 0 0.75rem 0;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.875rem;
|
||
}
|
||
}
|
||
|
||
.stage-stats {
|
||
display: flex;
|
||
align-items: baseline;
|
||
justify-content: center;
|
||
gap: 0.5rem;
|
||
margin-bottom: 0.5rem;
|
||
|
||
.stage-count {
|
||
font-size: 1.5rem;
|
||
font-weight: 700;
|
||
color: $slate-600;
|
||
line-height: 1;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 1.25rem;
|
||
}
|
||
}
|
||
|
||
.stage-label {
|
||
font-size: 0.75rem;
|
||
color: $slate-500;
|
||
font-weight: 500;
|
||
}
|
||
}
|
||
|
||
.stage-percentage {
|
||
font-size: 0.75rem;
|
||
color: $slate-400;
|
||
font-weight: 500;
|
||
background: $slate-100;
|
||
padding: 0.25rem 0.75rem;
|
||
border-radius: 1rem;
|
||
display: inline-block;
|
||
}
|
||
}
|
||
|
||
// Task List Styles
|
||
.task-body {
|
||
padding: 1rem;
|
||
min-height: 15vh;
|
||
max-height: 50vh;
|
||
background: $white;
|
||
border-radius: 1rem;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||
overflow-y: auto;
|
||
|
||
@media (max-width: 768px) {
|
||
padding: 0 1rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
padding: 0 0.75rem;
|
||
}
|
||
}
|
||
|
||
.actionable-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.items-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(7, minmax(0, 250px));
|
||
gap: 0.45rem;
|
||
justify-content: center;
|
||
|
||
@media (max-width: 768px) {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 0.375rem;
|
||
}
|
||
}
|
||
|
||
.action-item {
|
||
background-color: $white;
|
||
padding: 0.25rem;
|
||
border-radius: 0.5rem;
|
||
border: 1px solid $slate-200;
|
||
cursor: pointer;
|
||
transition: all 0.2s ease-in-out;
|
||
min-height: 50px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
justify-content: space-between;
|
||
|
||
@media (max-width: 768px) {
|
||
padding: 0.75rem;
|
||
min-height: 90px;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
padding: 0.625rem;
|
||
min-height: 50px;
|
||
}
|
||
|
||
&:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 10px rgba(0,0,0,0.05);
|
||
|
||
@media (max-width: 768px) {
|
||
transform: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
&:active {
|
||
transform: scale(0.98);
|
||
background-color: $slate-50;
|
||
}
|
||
}
|
||
|
||
&.active {
|
||
background-color: #eef2ff;
|
||
border-color: $indigo;
|
||
}
|
||
|
||
.item-content {
|
||
display: flex;
|
||
align-items: flex-start;
|
||
gap: 0.75rem;
|
||
width: 100%;
|
||
|
||
@media (max-width: 768px) {
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
gap: 0.375rem;
|
||
}
|
||
}
|
||
|
||
.avatar-section {
|
||
flex-shrink: 0;
|
||
|
||
.avatar {
|
||
width: 2.5rem;
|
||
height: 2.5rem;
|
||
border-radius: 50%;
|
||
overflow: hidden;
|
||
background-color: $slate-200;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
|
||
@media (max-width: 768px) {
|
||
width: 2.25rem;
|
||
height: 2.25rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
width: 2rem;
|
||
height: 2rem;
|
||
}
|
||
|
||
.avatar-img {
|
||
width: 100%;
|
||
height: 100%;
|
||
object-fit: cover;
|
||
border-radius: 50%;
|
||
}
|
||
}
|
||
}
|
||
|
||
.info-section {
|
||
flex: 1;
|
||
min-width: 0; // 允许flex项目收缩
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 0.5rem;
|
||
|
||
@media (max-width: 768px) {
|
||
gap: 0.375rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
gap: 0.25rem;
|
||
}
|
||
}
|
||
|
||
.item-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-start;
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.item-footer {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: flex-end;
|
||
flex-wrap: wrap;
|
||
gap: 0.5rem;
|
||
|
||
@media (max-width: 768px) {
|
||
gap: 0.375rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
gap: 0.25rem;
|
||
}
|
||
}
|
||
|
||
.item-name-group {
|
||
display: flex;
|
||
align-items: center;
|
||
flex: 1;
|
||
min-width: 0; // 允许收缩
|
||
|
||
.icon {
|
||
margin-right: 0.5rem;
|
||
flex-shrink: 0;
|
||
}
|
||
|
||
.item-name {
|
||
font-weight: 600;
|
||
font-size: 0.875rem;
|
||
line-height: 1.3;
|
||
color: $slate-800;
|
||
|
||
// 处理名字过长的情况
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
white-space: nowrap;
|
||
max-width: 100%;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 0.75rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
.item-time {
|
||
font-size: 0.75rem;
|
||
color: $slate-500;
|
||
flex-shrink: 0;
|
||
white-space: nowrap;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.6875rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 0.625rem;
|
||
}
|
||
}
|
||
|
||
.health-status {
|
||
font-size: 0.75rem;
|
||
font-weight: bold;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.6875rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 0.625rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
// Profession and Education
|
||
.profession-education {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.2rem;
|
||
flex-wrap: wrap;
|
||
|
||
@media (max-width: 480px) {
|
||
gap: 0.5rem;
|
||
}
|
||
|
||
.profession, .education {
|
||
font-size: 0.75rem;
|
||
border-radius: 0.3rem;
|
||
font-weight: 500;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.75rem;
|
||
padding: 0.1875rem 0.375rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 0.75rem;
|
||
padding: 0.125rem 0.25rem;
|
||
}
|
||
}
|
||
|
||
.profession {
|
||
background-color: #e0f2fe;
|
||
color: #0277bd;
|
||
}
|
||
|
||
.education {
|
||
background-color: #f3e5f5;
|
||
color: #7b1fa2;
|
||
}
|
||
|
||
.course-type {
|
||
background-color: #e8f5e8;
|
||
color: #2e7d32;
|
||
font-size: 0.75rem;
|
||
padding: 0.1rem 0.5rem;
|
||
border-radius: 0.3rem;
|
||
font-weight: 500;
|
||
display: inline-block;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.75rem;
|
||
padding: 0.1875rem 0.375rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 0.75rem;
|
||
padding: 0.125rem 0.25rem;
|
||
margin-left: 0.375rem;
|
||
}
|
||
}
|
||
|
||
.course-attendance {
|
||
display: inline-block;
|
||
// margin-top: 0.25rem;
|
||
padding: 0.1rem 0.25rem;
|
||
background: linear-gradient(135deg, #e3f2fd, #bbdefb);
|
||
border-radius: 0.3rem;
|
||
font-size: 0.75rem;
|
||
color: #1565c0;
|
||
font-weight: 500;
|
||
border: 1px solid #90caf9;
|
||
|
||
@media (max-width: 768px) {
|
||
font-size: 0.75rem;
|
||
padding: 0.1875rem 0.375rem;
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
font-size: 0.75rem;
|
||
padding: 0.125rem 0.25rem;
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
// Health status styles
|
||
|
||
// Clickable styles
|
||
.clickable {
|
||
cursor: pointer;
|
||
color: #1976d2;
|
||
text-decoration: underline;
|
||
transition: color 0.2s ease;
|
||
|
||
&:hover {
|
||
color: #1565c0;
|
||
text-decoration: none;
|
||
}
|
||
}
|
||
|
||
/* 弹窗样式 */
|
||
.modal-overlay {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
background-color: rgba(0, 0, 0, 0.5);
|
||
display: flex;
|
||
justify-content: center;
|
||
align-items: center;
|
||
z-index: 1000;
|
||
}
|
||
|
||
.modal-content {
|
||
background: white;
|
||
border-radius: 8px;
|
||
width: 90%;
|
||
max-width: 600px;
|
||
max-height: 80vh;
|
||
overflow: hidden;
|
||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.modal-header {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 20px;
|
||
border-bottom: 1px solid #e5e7eb;
|
||
background-color: #f9fafb;
|
||
}
|
||
|
||
.modal-header h3 {
|
||
margin: 0;
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #374151;
|
||
}
|
||
|
||
.close-btn {
|
||
background: none;
|
||
border: none;
|
||
font-size: 24px;
|
||
cursor: pointer;
|
||
color: #6b7280;
|
||
padding: 0;
|
||
width: 30px;
|
||
height: 30px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
border-radius: 4px;
|
||
transition: background-color 0.2s;
|
||
}
|
||
|
||
.close-btn:hover {
|
||
background-color: #e5e7eb;
|
||
color: #374151;
|
||
}
|
||
|
||
.modal-body {
|
||
padding: 20px;
|
||
max-height: 60vh;
|
||
overflow-y: auto;
|
||
}
|
||
|
||
.no-messages {
|
||
text-align: center;
|
||
color: #6b7280;
|
||
font-size: 16px;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.messages-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12px;
|
||
}
|
||
|
||
.message-item {
|
||
display: flex;
|
||
gap: 8px;
|
||
padding: 12px;
|
||
background-color: #f9fafb;
|
||
border-radius: 6px;
|
||
border-left: 3px solid #3b82f6;
|
||
}
|
||
|
||
.message-number {
|
||
font-weight: 600;
|
||
color: #3b82f6;
|
||
min-width: 20px;
|
||
}
|
||
|
||
.message-content {
|
||
flex: 1;
|
||
line-height: 1.5;
|
||
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> |