feat(person): 在销售时间轴任务列表中增加已完成用户分组显示
- 将待操作用户列表重构为独立的任务区块,根据选定阶段动态显示标题 - 新增已完成用户区块,根据销售阶段自动计算已完成联系人 - 为不同区块添加视觉区分样式,包括标题颜色和背景 - 添加阶段排序逻辑和表单完成状态检查函数 - 改进联系人数据构建函数以支持多种数据源
This commit is contained in:
@@ -205,38 +205,84 @@
|
|||||||
|
|
||||||
<div class="task-body">
|
<div class="task-body">
|
||||||
<div class="actionable-list">
|
<div class="actionable-list">
|
||||||
<div class="items-grid">
|
<div class="task-section">
|
||||||
<div
|
<div class="task-section-header">
|
||||||
v-for="item in contacts"
|
<span class="task-section-title task-section-title-pending">{{ pendingTitle }}</span>
|
||||||
:key="item.id"
|
<span class="task-section-count">{{ contacts.length }}位</span>
|
||||||
:id="`contact-item-${item.id}`"
|
</div>
|
||||||
class="action-item"
|
<div class="items-grid">
|
||||||
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
|
<div
|
||||||
@click="selectContact(item.id)">
|
v-for="item in contacts"
|
||||||
<div class="item-content">
|
:key="item.id"
|
||||||
<!-- 头像区域 -->
|
:id="`contact-item-${item.id}`"
|
||||||
<div class="avatar-section">
|
class="action-item"
|
||||||
<div class="avatar">
|
:class="[getHealthClass(item.health), { 'active': item.id === selectedContactId }]"
|
||||||
<img :src="item.avatar || '/default-avatar.svg'" :alt="item.name" class="avatar-img" />
|
@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 class="info-section">
|
</div>
|
||||||
<div class="item-header">
|
<div v-if="completedContacts.length > 0" class="task-section">
|
||||||
<div class="item-name-group">
|
<div class="task-section-header">
|
||||||
<span class="item-name" :title="item.name">{{ item.name }}</span>
|
<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>
|
||||||
<span class="item-time">{{ item.time }}</span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item-footer">
|
|
||||||
<div class="profession-education">
|
<div class="info-section">
|
||||||
<span class="profession">{{ item.profession||'未知' }}</span>
|
<div class="item-header">
|
||||||
<span class="education">{{ item.education||'未知' }}</span>
|
<div class="item-name-group">
|
||||||
<span class="course-attendance">
|
<span class="item-name" :title="item.name">{{ item.name }}</span>
|
||||||
{{ getAttendedLessons(item.class_situation,item.class_num) }}
|
</div>
|
||||||
</span>
|
<span class="item-time">{{ item.time }}</span>
|
||||||
<span class="course-type" v-if="item.type">{{ item.type }}</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>
|
||||||
@@ -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%); } }
|
||||||
@@ -1054,4 +1189,4 @@ $indigo: #4f46e5;
|
|||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user