feat(销售时间线): 重构销售时间线组件并优化API调用
重构销售时间线组件,增加对客户阶段数据的动态计算和展示 优化API端点配置,新增课1-4后阶段和成交客户的数据接口 调整样式和布局,增加客户类型标签显示 延长HTTP请求超时时间至100秒
This commit is contained in:
@@ -16,7 +16,7 @@ export const getTableFillingRate = (params) => {
|
||||
}
|
||||
|
||||
// 平均应答时间 /api/v1/more_level_screening/average_response_time
|
||||
export const getAverageResponseTime = (params) => {
|
||||
export const getAverageResponseTime = (params) => {
|
||||
return https.post('/api/v1/sales/average_response_time', params)
|
||||
}
|
||||
|
||||
@@ -28,7 +28,6 @@ export const getWeeklyActiveCommunicationRate = (params) => {
|
||||
// 超时应答率 /api/v1/sales/timeout_response_rate
|
||||
export const getTimeoutResponseRate = (params) => {
|
||||
return https.post('/api/v1/sales/timeout_response_rate', params)
|
||||
|
||||
}
|
||||
|
||||
// 客户通话录音 /api/v1/sales/get_customer_call_info
|
||||
@@ -46,10 +45,6 @@ export const getCustomerFormInfo = (params) => {
|
||||
return https.post('/api/v1/customer_list/get_customer_form_info', params)
|
||||
}
|
||||
|
||||
// 时间线 /api/v1/customer_list/sales_customers_list
|
||||
export const getCustomerAttendance = (params) => {
|
||||
return https.post('/api/v1/customer_list/sales_customers_list', params)
|
||||
}
|
||||
// 转化率、分配数据量、加微率 /api/v1/sales/conversion_rate_and_allocated_data
|
||||
export const getConversionRateAndAllocatedData = (params) => {
|
||||
return https.post('/api/v1/sales/conversion_rate_and_allocated_data', params)
|
||||
@@ -57,5 +52,21 @@ export const getConversionRateAndAllocatedData = (params) => {
|
||||
|
||||
|
||||
|
||||
// 时间线 /api/v1/customer_list/sales_customers_list
|
||||
export const getCustomerAttendance = (params ) => {
|
||||
return https.post('/api/v1/sales_timeline/get_all_customers', params)
|
||||
}
|
||||
|
||||
// 课1-4之后的4个阶段 /api/v1/sales_timeline/get_class_customers
|
||||
export const getCustomerAttendanceAfterClass4 = (params) => {
|
||||
return https.post('/api/v1/sales_timeline/get_class_customers', params)
|
||||
}
|
||||
|
||||
// 成交 /api/v1/sales_timeline/get_pay_money_customers
|
||||
export const getPayMoneyCustomers = (params) => {
|
||||
return https.post('/api/v1/sales_timeline/get_pay_money_customers', params)
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useUserStore } from '@/stores/user'
|
||||
// 创建axios实例
|
||||
const service = axios.create({
|
||||
baseURL: 'http://192.168.15.51:8890' || '', // API基础路径,支持完整URL
|
||||
timeout: 30000, // 请求超时时间
|
||||
timeout: 100000, // 请求超时时间
|
||||
headers: {
|
||||
'Content-Type': 'application/json;charset=UTF-8'
|
||||
}
|
||||
@@ -157,7 +157,7 @@ const http = {
|
||||
},
|
||||
|
||||
// POST请求
|
||||
post(url, data = {}, config = {}) {
|
||||
post(url, data, config) {
|
||||
return service({
|
||||
method: 'post',
|
||||
url,
|
||||
|
||||
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -60,7 +60,6 @@
|
||||
<div class="loading-spinner"></div>
|
||||
<div class="loading-text">正在加载团队详情...</div>
|
||||
</div>
|
||||
|
||||
<!-- 团队详情内容 -->
|
||||
<div v-else>
|
||||
<div class="team-detail-header">
|
||||
|
||||
Reference in New Issue
Block a user