feat(person): 在销售时间轴任务列表中增加已完成用户分组显示

- 将待操作用户列表重构为独立的任务区块,根据选定阶段动态显示标题
- 新增已完成用户区块,根据销售阶段自动计算已完成联系人
- 为不同区块添加视觉区分样式,包括标题颜色和背景
- 添加阶段排序逻辑和表单完成状态检查函数
- 改进联系人数据构建函数以支持多种数据源
This commit is contained in:
2026-01-23 18:07:34 +08:00
parent baa89001c8
commit 2cd59adfc9

View File

@@ -205,6 +205,11 @@
<div class="task-body"> <div class="task-body">
<div class="actionable-list"> <div class="actionable-list">
<div class="task-section">
<div class="task-section-header">
<span class="task-section-title task-section-title-pending">{{ pendingTitle }}</span>
<span class="task-section-count">{{ contacts.length }}</span>
</div>
<div class="items-grid"> <div class="items-grid">
<div <div
v-for="item in contacts" v-for="item in contacts"
@@ -214,14 +219,12 @@
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]" :class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
@click="selectContact(item.id)"> @click="selectContact(item.id)">
<div class="item-content"> <div class="item-content">
<!-- 头像区域 -->
<div class="avatar-section"> <div class="avatar-section">
<div class="avatar"> <div class="avatar">
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" /> <img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
</div> </div>
</div> </div>
<!-- 信息区域 -->
<div class="info-section"> <div class="info-section">
<div class="item-header"> <div class="item-header">
<div class="item-name-group"> <div class="item-name-group">
@@ -244,6 +247,49 @@
</div> </div>
</div> </div>
</div> </div>
<div v-if="completedContacts.length > 0" class="task-section">
<div class="task-section-header">
<span class="task-section-title task-section-title-completed">已完成用户</span>
<span class="task-section-count">{{ completedContacts.length }}</span>
</div>
<div class="items-grid">
<div
v-for="item in completedContacts"
:key="item.id"
:id="`completed-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> </div>
<!-- 到课详情区域 --> <!-- 到课详情区域 -->
@@ -649,6 +695,11 @@ const totalCustomers = computed(() => {
return Math.max(...baseStages, 1); return Math.max(...baseStages, 1);
}); });
const pendingTitle = computed(() => {
if (props.selectedStage === '全部' || props.selectedStage === 'all') return '全部用户';
return '待操作用户';
});
const getAttendedLessons = (classSituation, classNum) => { const getAttendedLessons = (classSituation, classNum) => {
if (classNum && Array.isArray(classNum) && classNum.length > 0) { if (classNum && Array.isArray(classNum) && classNum.length > 0) {
const filtered = classNum.filter(n => n !== -1); const filtered = classNum.filter(n => n !== -1);
@@ -666,6 +717,84 @@ const getAttendedLessons = (classSituation, classNum) => {
return '未到课'; return '未到课';
}; };
const stageOrder = {
'待加微': 1,
'待填表单': 2,
'待入群': 3,
'待联系': 4,
'待到课': 5,
'课1-4': 6,
'点击未支付': 7,
'付定金': 8,
'定金转化': 9,
'成交': 10
};
const getStageIndex = (type) => stageOrder[type] || 0;
const isFormIncomplete = (customer) => {
const occupationMissing = customer.customer_occupation === '未知' || !customer.customer_occupation;
const educationMissing = customer.customer_child_education === '未知' || !customer.customer_child_education;
return occupationMissing || educationMissing;
};
const buildContacts = (customers, stageFallback) => {
return (customers || []).map(customer => ({
id: customer.customer_name || customer.id || customer.name,
name: customer.customer_name || customer.name,
phone: customer.phone,
profession: customer.customer_occupation || customer.profession,
education: customer.customer_child_education || customer.education,
lastMessageTime: customer.latest_message_time || customer.time,
avatarUrl: customer.customer_avatar_url || customer.avatar || customer.weChat_avatar,
avatar: customer.customer_avatar_url || customer.avatar || customer.weChat_avatar || '/default-avatar.svg',
type: customer.type || stageFallback,
classNum: customer.class_num,
class_num: customer.class_num,
salesStage: customer.type || stageFallback,
priority: customer.type === '待联系' ? 'high' : 'normal',
time: customer.latest_message_time || customer.time || '未知',
health: customer.health || 75,
customer_name: customer.customer_name,
customer_occupation: customer.customer_occupation,
customer_child_education: customer.customer_child_education,
scrm_user_main_code: customer.scrm_user_main_code,
weChat_avatar: customer.weChat_avatar,
class_situation: customer.class_situation,
records: customer.records,
time_and_camp_stage: customer.time_and_camp_stage || []
})).filter(customer => customer.id);
};
const completedContacts = computed(() => {
const stage = props.selectedStage;
if (!stage || stage === '全部' || stage === 'all' || stage === '课1-4' || stage === '成交') {
return [];
}
if (stage === '待填表单') {
const baseCustomers = (props.customersList || []).filter(c => c.type !== '待加微');
const completed = baseCustomers.filter(c => !isFormIncomplete(c));
return buildContacts(completed, stage);
}
if (stage === '待到课') {
const baseCustomers = (props.customersList || []).filter(c => c.type !== '待加微');
const completed = baseCustomers.filter(c => getAttendedLessons(c.class_situation, c.class_num) !== '未到课');
return buildContacts(completed, stage);
}
if (['点击未支付', '付定金', '定金转化'].includes(stage)) {
const courseCustomers = props.courseCustomers?.['课1-4'] || [];
let completed = courseCustomers.filter(c => getStageIndex(c.type) > getStageIndex(stage));
if (stage === '定金转化') {
const payList = (props.payMoneyCustomersList || []).map(c => ({ ...c, type: '成交' }));
completed = completed.concat(payList);
}
return buildContacts(completed, stage);
}
const baseCustomers = props.customersList || [];
const completed = baseCustomers.filter(c => getStageIndex(c.type) > getStageIndex(stage));
return buildContacts(completed, stage);
});
// **修改 getStageCount 函数** // **修改 getStageCount 函数**
const getStageCount = (stageType) => { const getStageCount = (stageType) => {
@@ -861,7 +990,13 @@ $indigo: #4f46e5;
.stage-content { text-align: center; width: 100%; .stage-title { font-size: 1rem; font-weight: 500; color: $slate-700; margin: 0 0 0.75rem 0; } .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; } .stage-label { font-size: 0.75rem; color: $slate-500; } } .stage-percentage { font-size: 0.75rem; color: $slate-400; background: $slate-100; padding: 0.25rem 0.75rem; border-radius: 1rem; display: inline-block; } } .stage-content { text-align: center; width: 100%; .stage-title { font-size: 1rem; font-weight: 500; color: $slate-700; margin: 0 0 0.75rem 0; } .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; } .stage-label { font-size: 0.75rem; color: $slate-500; } } .stage-percentage { font-size: 0.75rem; color: $slate-400; background: $slate-100; padding: 0.25rem 0.75rem; border-radius: 1rem; display: inline-block; } }
.course-details { background: $white; border-radius: 0.75rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); margin-top: 1rem; .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; padding: 1rem 0; } .course-lessons { display: flex; flex-direction: row; gap: 0.75rem; .lesson-item { background: $slate-50; border-radius: 0.5rem; padding: 0.75rem; .lesson-header .lesson-number { font-weight: 600; } .lesson-details { display: flex; flex-wrap: wrap; gap: 1rem; .detail-item .detail-label { color: $slate-500; } .detail-item .detail-value { font-weight: 600; } } } } } } .course-details { background: $white; border-radius: 0.75rem; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); margin-top: 1rem; .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; padding: 1rem 0; } .course-lessons { display: flex; flex-direction: row; gap: 0.75rem; .lesson-item { background: $slate-50; border-radius: 0.5rem; padding: 0.75rem; .lesson-header .lesson-number { font-weight: 600; } .lesson-details { display: flex; flex-wrap: wrap; gap: 1rem; .detail-item .detail-label { color: $slate-500; } .detail-item .detail-value { font-weight: 600; } } } } } }
.task-body { padding: 1rem; max-height: 50vh; background: $white; border-radius: 1rem; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); overflow-y: auto; } .task-body { padding: 1rem; max-height: 50vh; background: $white; border-radius: 1rem; box-shadow: 0 4px 20px rgba(0, 0, 0, 0.08); overflow-y: auto; }
.actionable-list { display: flex; flex-direction: column; } .actionable-list { display: flex; flex-direction: column; gap: 1rem; }
.task-section { display: flex; flex-direction: column; gap: 0.5rem; }
.task-section-header { display: flex; align-items: center; gap: 0.5rem; }
.task-section-title { font-size: 0.85rem; font-weight: 600; padding: 0.2rem 0.6rem; border-radius: 999px; }
.task-section-title-pending { color: #16a34a; background: #dcfce7; }
.task-section-title-completed { color: #2563eb; background: #dbeafe; }
.task-section-count { font-size: 0.75rem; color: $slate-500; }
.items-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 250px)); gap: 0.45rem; justify-content: center; } .items-grid { display: grid; grid-template-columns: repeat(7, minmax(0, 250px)); gap: 0.45rem; justify-content: center; }
.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; display: flex; &:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.05); } &.active { background-color: #eef2ff; border-color: $indigo; } .item-content { display: flex; align-items: flex-start; gap: 0.75rem; width: 100%; } .avatar-section .avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; overflow: hidden; background-color: $slate-200; .avatar-img { width: 100%; height: 100%; object-fit: cover; } } .info-section { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; } .item-header { display: flex; justify-content: space-between; align-items: flex-start; } .item-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-time { font-size: 0.75rem; color: $slate-500; flex-shrink: 0; } .profession-education { display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; .profession, .education, .course-type, .course-attendance { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 0.3rem; font-weight: 500; } .profession { background-color: #e0f2fe; color: #0277bd; } .education { background-color: #f3e5f5; color: #7b1fa2; } .course-type { background-color: #e8f5e8; color: #2e7d32; } .course-attendance { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #1565c0; border: 1px solid #90caf9; } } } .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; display: flex; &:hover { transform: translateY(-2px); box-shadow: 0 4px 10px rgba(0,0,0,0.05); } &.active { background-color: #eef2ff; border-color: $indigo; } .item-content { display: flex; align-items: flex-start; gap: 0.75rem; width: 100%; } .avatar-section .avatar { width: 2.5rem; height: 2.5rem; border-radius: 50%; overflow: hidden; background-color: $slate-200; .avatar-img { width: 100%; height: 100%; object-fit: cover; } } .info-section { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 0.5rem; } .item-header { display: flex; justify-content: space-between; align-items: flex-start; } .item-name { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .item-time { font-size: 0.75rem; color: $slate-500; flex-shrink: 0; } .profession-education { display: flex; align-items: center; gap: 0.2rem; flex-wrap: wrap; .profession, .education, .course-type, .course-attendance { font-size: 0.75rem; padding: 0.1rem 0.5rem; border-radius: 0.3rem; font-weight: 500; } .profession { background-color: #e0f2fe; color: #0277bd; } .education { background-color: #f3e5f5; color: #7b1fa2; } .course-type { background-color: #e8f5e8; color: #2e7d32; } .course-attendance { background: linear-gradient(135deg, #e3f2fd, #bbdefb); color: #1565c0; border: 1px solid #90caf9; } } }
.unassigned-recordings-btn { background-color: $primary; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; &:hover { background-color: darken($primary, 10%); } } .unassigned-recordings-btn { background-color: $primary; color: white; border: none; padding: 8px 16px; border-radius: 4px; cursor: pointer; transition: background-color 0.3s ease; &:hover { background-color: darken($primary, 10%); } }