feat(销售时间线): 重构销售时间线组件并优化API调用

重构销售时间线组件,增加对客户阶段数据的动态计算和展示
优化API端点配置,新增课1-4后阶段和成交客户的数据接口
调整样式和布局,增加客户类型标签显示
延长HTTP请求超时时间至100秒
This commit is contained in:
2025-08-13 20:33:13 +08:00
parent 233b7311fe
commit 5de287e777
5 changed files with 619 additions and 1142 deletions

View File

@@ -19,10 +19,10 @@
<div class="stage-content">
<h3 class="stage-title">{{ stage.displayName || stage.name }}</h3>
<div class="stage-stats">
<span class="stage-count">{{ stage.count }}</span>
<span class="stage-count">{{ stage.name === '全部' ? customersCount : stage.count }}</span>
<span class="stage-label">位客户</span>
</div>
<div class="stage-percentage">{{ getPercentage(stage.count) }}%</div>
<div v-if="stage.name !== '全部'" class="stage-percentage">{{ getPercentage(stage.count) }}%</div>
</div>
</div>
</div>
@@ -58,12 +58,10 @@
<div class="profession-education">
<span class="profession">{{ item.profession || '公务员' }}</span>
<span class="education">{{ item.education || '高中' }}</span>
<!-- 课程相关信息 -->
<span v-if="item.salesStage === '全部' || item.salesStage === '课1-4' || item.salesStage === '点击未支付' || item.salesStage === '付定金' || item.salesStage === '定价转化' || item.salesStage === '成交'">
<span class="course-attendance">
{{ getAttendedLessons(item.class_situation) }}
</span>
<span class="course-attendance">
{{ getAttendedLessons(item.class_situation) }}
</span>
<span class="course-type">{{ item.type }}</span>
</div>
</div>
</div>
@@ -117,7 +115,6 @@ import { computed, ref } from 'vue';
// 定义props
const props = defineProps({
// SalesTimeline props
data: {
type: Object,
default: () => ({})
@@ -126,7 +123,6 @@ const props = defineProps({
type: String,
default: 'all'
},
// TaskList props
contacts: {
type: Array,
required: true
@@ -135,28 +131,34 @@ const props = defineProps({
type: Number,
default: null
},
// 课程客户数据课1-4
courseCustomers: {
type: Object,
default: () => ({})
},
// KPI数据转化率、分配数据量、加微率等
kpiData: {
type: Object,
default: () => ({})
customersList: {
type: Array,
default: () => []
},
customersCount: {
type: Number,
default: 0
},
payMoneyCustomersList: {
type: Array,
default: () => []
},
payMoneyCustomersCount: {
type: Number,
default: 0
}
});
// 定义emits
const emit = defineEmits(['stage-select', 'select-contact']);
// SalesTimeline methods
// 计算总客户数
const totalCustomers = computed(() => {
// 使用 "全部" 字段作为总客户数,如果没有则使用所有阶段的最大值
if (props.data['全部']) {
return props.data['全部'];
}
if (props.customersCount > 0) return props.customersCount;
if (props.customersList?.length > 0) return props.customersList.length;
if (props.data['全部']) return props.data['全部'];
const baseStages = [
props.data['未加微'] || 0,
@@ -169,53 +171,76 @@ const totalCustomers = computed(() => {
props.data['定价转化'] || 0,
props.data['成交'] || 0
];
return Math.max(...baseStages, 1); // 至少返回1避免除零错误
return Math.max(...baseStages, 1);
});
const getStageCount = (stageType) => {
if (!props.customersList?.length) {
return props.data[stageType] || 0;
}
if (stageType === '课1-4' && props.courseCustomers?.['课1-4']) {
return props.courseCustomers['课1-4'].length;
}
return props.customersList.filter(customer => customer.type === stageType).length;
};
const stages = computed(() => {
const stageList = [
{ id: 0, name: '全部', displayName: '全部', count: props.customersCount || props.customersList.length, 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: '#64b5f6' },
{ 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 stages = computed(() => [
{ id: 0, name: '全部', displayName: '全部', count: props.data['全部'] || 0, color: '#f3f4f6' },
{ id: 1, name: '待加微', displayName: '待加微', count: props.data['未加微'] || 0, color: '#e3f2fd' },
{ id: 2, name: '待填表单', displayName: '待填表单', count: props.data['待填表单'] || 0, color: '#90caf9' },
{ id: 3, name: '待入群', displayName: '待入群', count: props.data['待入群'] || 0, color: '#bbdefb' },
{ id: 4, name: '待联系', displayName: '待联系', count: props.data['待联系'] || 0, color: '#bbdefb' },
{ id: 5, name: '待到课', displayName: '待到课', count: props.data['待到课'] || 0, color: '#bbdefb' },
{ id: 6, name: '课1-4', displayName: '课1-4', count: props.data['课1-4'] || 0, color: '#64b5f6' },
{ id: 7, name: '点击未支付', displayName: '点击未支付', count: props.data['点击未支付'] || 0, color: '#42a5f5' },
{ id: 8, name: '付定金', displayName: '付定金', count: props.data['付定金'] || 0, color: '#2196f3' },
{ id: 9, name: '定价转化', displayName: '定价转化', count: props.data['定价转化'] || 0, color: '#1e88e5' },
{ id: 10, name: '成交', displayName: '成交', count: props.data['成交'] || 0, color: '#1976d2' }
]);
// 计算百分比
const getPercentage = (count) => {
if (totalCustomers.value === 0) return 0;
return Math.round((count / totalCustomers.value) * 100);
};
// 选择阶段
const selectStage = (stageName) => {
// 如果选择的是课1-4阶段使用独立的课程数据处理逻辑
if (stageName === '课1-4') {
console.log('选择课1-4阶段课程数据:', props.courseCustomers);
// 发送课程数据选择事件,包含课程客户数据
let filteredCustomers = [];
if (stageName === '全部') {
filteredCustomers = props.customersList || [];
} else if (stageName === '课1-4' && props.courseCustomers?.['课1-4']) {
emit('stage-select', stageName, {
isCourseStage: true,
courseData: props.courseCustomers['课1-4'] || []
courseData: props.courseCustomers['课1-4'],
filteredCustomers: props.courseCustomers['课1-4']
});
return;
} else {
// 普通阶段选择
emit('stage-select', stageName);
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);
});
// TaskList methods
const selectContact = (id) => {
emit('select-contact', id);
};
@@ -226,39 +251,29 @@ const getHealthIndicator = (score) => {
return { class: 'health-risk', text: '高风险', textColor: 'text-red' };
};
// 获取客户参加的课程情况
const getAttendedLessons = (classSituation) => {
if (!classSituation) {
return '暂无到课记录';
}
if (!classSituation) return '暂无到课记录';
// 如果是数组,直接展示
if (Array.isArray(classSituation)) {
return classSituation.join(' ');
}
// 如果是对象,使用现有的处理方式
if (typeof classSituation === 'object') {
// 提取课程编号并排序
const lessonNumbers = Object.keys(classSituation)
.map(key => parseInt(key))
.filter(num => !isNaN(num))
.sort((a, b) => a - b);
if (lessonNumbers.length === 0) {
return '暂无到课记录';
}
return `${lessonNumbers.join(' ')}`;
return lessonNumbers.length > 0 ? lessonNumbers.join(' ') : '暂无到课记录';
}
// 其他情况
return '暂无到课记录';
};
</script>
<style lang="scss" scoped>
// Color Palette
// Color Variables
$slate-50: #f8fafc;
$slate-100: #f1f5f9;
$slate-200: #e2e8f0;
@@ -270,17 +285,11 @@ $slate-700: #334155;
$slate-800: #1e293b;
$white: #ffffff;
$blue: #3b82f6;
$green: #22c55e;
$amber: #f59e0b;
$red: #ef4444;
$indigo: #4f46e5;
$purple: #a855f7;
$primary: #3b82f6;
$success: #22c55e;
$warning: #f59e0b;
$danger: #ef4444;
$indigo: #4f46e5;
// 到课详情区域样式
.course-details {
@@ -338,27 +347,27 @@ $danger: #ef4444;
}
.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;
}
}
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;
}
}
}
}
}
@@ -386,18 +395,18 @@ $danger: #ef4444;
padding: 0.625rem;
.lesson-details {
gap: 0.75rem;
.detail-item {
.detail-label {
font-size: 0.6875rem;
}
.detail-value {
font-size: 0.8125rem;
}
}
}
gap: 0.75rem;
.detail-item {
.detail-label {
font-size: 0.6875rem;
}
.detail-value {
font-size: 0.8125rem;
}
}
}
}
}
}
@@ -889,6 +898,28 @@ $danger: #ef4444;
color: #7b1fa2;
}
.course-type {
background-color: #e8f5e8;
color: #2e7d32;
font-size: 0.75rem;
padding: 0.125rem 0.5rem;
border-radius: 0.375rem;
font-weight: 500;
display: inline-block;
margin-left: 0.5rem;
@media (max-width: 768px) {
font-size: 0.6875rem;
padding: 0.1875rem 0.375rem;
}
@media (max-width: 480px) {
font-size: 0.625rem;
padding: 0.125rem 0.25rem;
margin-left: 0.375rem;
}
}
.course-attendance {
display: inline-block;
// margin-top: 0.25rem;
@@ -912,139 +943,10 @@ $danger: #ef4444;
}
}
// Tags
.tags-container {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 0.5rem;
}
.tag {
font-size: 0.75rem;
font-weight: 600;
padding: 0.125rem 0.625rem;
border-radius: 9999px;
&.tag-high-intent { background-color: #fee2e2; color: #991b1b; }
&.tag-super-hot { background-color: #fef3c7; color: #92400e; }
&.tag-no-follow-up { background-color: #fef9c3; color: #92400e; }
&.tag-sleeping { background-color: #dbeafe; color: #1e40af; }
&.concern-tag { background-color: $slate-200; color: $slate-700; }
}
// Health Status Colors
.health-good { border-color: $green; }
.health-ok { border-color: $amber; }
.health-risk { border-color: $red; }
.text-green { color: $green; }
.text-amber { color: $amber; }
.text-red { color: $red; }
.health-good { border-color: $success; }
.health-ok { border-color: $warning; }
.health-risk { border-color: $danger; }
// Icon SVG helper
:deep(.icon-svg) {
width: 1.25rem;
height: 1.25rem;
}
:deep(.icon) {
display: flex;
align-items: center;
}
:deep(.icon-svg) {
&.high { color: $red; }
&.recommended { color: $blue; }
&.new { color: $green; }
}
/* 移动端优化 */
@media (max-width: 768px) {
.task-body {
padding: 0.75rem;
max-height: 400px;
}
.items-grid {
gap: 0.5rem;
}
.action-item {
padding: 0.75rem;
.item-header {
flex-direction: column;
align-items: flex-start;
gap: 0.25rem;
.item-name {
font-size: 0.875rem;
}
.item-time {
font-size: 0.75rem;
align-self: flex-end;
}
}
.item-tags {
gap: 0.25rem;
flex-wrap: wrap;
.tag {
padding: 0.125rem 0.375rem;
font-size: 0.625rem;
}
}
.item-health {
margin-top: 0.5rem;
.health-score {
font-size: 0.75rem;
}
}
}
}
/* 小屏幕优化 */
@media (max-width: 480px) {
.task-body {
padding: 0.5rem;
max-height: 300px;
}
.action-item {
padding: 0.5rem;
.item-header {
.item-name {
font-size: 0.75rem;
line-height: 1.2;
}
.item-time {
font-size: 0.625rem;
}
}
.item-tags {
margin-top: 0.25rem;
.tag {
padding: 0.0625rem 0.25rem;
font-size: 0.5rem;
}
}
.item-health {
margin-top: 0.25rem;
.health-score {
font-size: 0.625rem;
}
.health-bar {
height: 3px;
}
}
}
}
</style>