Compare commits

...

2 Commits

Author SHA1 Message Date
d75fd9beb8 fix: 修复表单数据处理逻辑并更新UI文本显示
修复表单数据从对象到数组的格式兼容处理,确保新旧数据格式都能正确显示
更新多处UI文本描述,包括"今日通话"改为"本期通话"、"月度总业绩"改为"本月成交单数"
调整客户阶段筛选逻辑,排除'待加微'客户对'待填表单'和'待到课'的影响
修复未分配通话记录API端点错误
2025-10-22 16:22:51 +08:00
094f655634 fix: 修复表单数据处理和API端点问题
修复RawDataCards和CustomerDetail组件中表单数据的默认值和类型定义
更新getTodayCall API端点路径为current_camp_call
调整sale.vue中表单数据处理逻辑以适应新API响应格式
优化PerformanceComparison组件的数据处理逻辑
修改开发环境API基础路径为本地测试地址
2025-10-22 14:57:28 +08:00
12 changed files with 184 additions and 182 deletions

View File

@@ -7,7 +7,7 @@ export const getProblemDistribution = (params) => {
// 今日通话 /api/v1/more_level_screening/today_call // 今日通话 /api/v1/more_level_screening/today_call
export const getTodayCall = (params) => { export const getTodayCall = (params) => {
return https.post('/api/v1/sales/today_call', params) return https.post('/api/v1/sales/current_camp_call', params)
} }
// 表格填写率 /api/v1/more_level_screening/table_filling_rate // 表格填写率 /api/v1/more_level_screening/table_filling_rate

View File

@@ -74,7 +74,10 @@ export const getAbnormalResponseRate = (params) => {
export const getHistoryCamps = (params) => { export const getHistoryCamps = (params) => {
return https.post('/api/v1/level_three/overview/get_history_camps', params) return https.post('/api/v1/level_three/overview/get_history_camps', params)
} }
// 数据对比 /api/v1/level_three/overview/get_team_many_target
export const getTeamManyTarget = (params) => {
return https.post('/api/v1/level_three/overview/get_team_many_target', params)
}

View File

@@ -5,8 +5,8 @@ import { useUserStore } from '@/stores/user'
// 创建axios实例 // 创建axios实例
const service = axios.create({ const service = axios.create({
baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL // baseURL: 'https://mldash.nycjy.cn/' || '', // API基础路径支持完整URL
// baseURL: 'http://192.168.15.121:8890' || '', // API基础路径支持完整URL baseURL: 'http://192.168.15.121:8890' || '', // API基础路径支持完整URL
timeout: 100000, // 请求超时时间 timeout: 100000, // 请求超时时间
headers: { headers: {
'Content-Type': 'application/json;charset=UTF-8' 'Content-Type': 'application/json;charset=UTF-8'

View File

@@ -32,7 +32,7 @@
</div> </div>
<div class="report-card"> <div class="report-card">
<div class="card-header"> <div class="card-header">
<span class="card-title">月度总业绩 <i class="info-icon" @mouseenter="showTooltip('monthlyRevenue', $event)" @mouseleave="hideTooltip"></i></span> <span class="card-title">本月成交单数 <i class="info-icon" @mouseenter="showTooltip('monthlyRevenue', $event)" @mouseleave="hideTooltip"></i></span>
<span class="card-trend positive">+8% vs 上月</span> <span class="card-trend positive">+8% vs 上月</span>
</div> </div>
<div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div> <div class="card-value">{{ formatCurrency(weekTotalData.week_add_fee_total?.total_add_fee || 0) }} </div>
@@ -122,8 +122,8 @@ const metricDescriptions = {
description: '本期新增的成交订单数量,已确认付款或签约的客户订单。' description: '本期新增的成交订单数量,已确认付款或签约的客户订单。'
}, },
monthlyRevenue: { monthlyRevenue: {
title: '月度总业绩', title: '本月成交单数',
description: '本月团队累计完成的销售业绩总额,包括所有已确认的订单金额。' description: '本月团队累计完成的销售订单数量,包括所有已确认的订单。'
}, },
conversionRate: { conversionRate: {
title: '定金转化率', title: '定金转化率',

View File

@@ -104,8 +104,8 @@ const props = defineProps({
default: null default: null
}, },
formInfo: { formInfo: {
type: Object, type: Array,
default: () => ({}) default: () => []
}, },
chatRecords: { chatRecords: {
type: Array, type: Array,
@@ -193,89 +193,34 @@ const startBasicAnalysis = async () => {
basicAnalysisResult.value = ''; basicAnalysisResult.value = '';
// 构建表单信息 // 构建表单信息
const formData = props.formInfo || {}; const formData = props.formInfo || [];
let formInfoText = '暂无表单信息'; let formInfoText = '暂无表单信息';
if (Object.keys(formData).length > 0) { // ** 适配新的 formInfo 数组格式 **
if (Array.isArray(formData) && formData.length > 0) {
const allInfo = []; const allInfo = [];
// 处理第一种格式基础信息和additional_info // 遍历新格式: [{ question_label: "...", answer: "..." }, ...]
if (formData.name || formData.mobile || formData.additional_info) { formData.forEach(item => {
const basicInfo = []; // 检查字段是否存在且答案有效
const additionalInfo = []; if (
item.question_label &&
const basicFields = { item.answer &&
name: '姓名', item.answer !== '暂无' &&
mobile: '手机号', item.answer !== ''
occupation: '职业', ) {
territory: '地区', // 格式化为 "问题标签: 答案"
child_name: '孩子姓名', allInfo.push(`${item.question_label.trim()}: ${item.answer.trim()}`);
child_gender: '孩子性别',
child_education: '孩子教育阶段',
child_relation: '与孩子关系'
};
Object.entries(basicFields).forEach(([key, label]) => {
if (formData[key] && formData[key] !== '暂无' && formData[key] !== '') {
basicInfo.push(`${label}: ${formData[key]}`);
}
});
if (formData.additional_info && Array.isArray(formData.additional_info)) {
formData.additional_info.forEach(item => {
if (item.topic && item.answer) {
additionalInfo.push(`${item.topic}\n答案: ${item.answer}`);
}
});
} }
});
if (basicInfo.length > 0) { // 格式化表单信息文本
allInfo.push('=== 基础信息 ==='); formInfoText = allInfo.length > 0
allInfo.push(...basicInfo); ? `=== 问卷/表单信息 ===\n${allInfo.join('\n')}`
} : '暂无有效问卷/表单信息';
if (additionalInfo.length > 0) {
allInfo.push('\n=== 问卷信息 ===');
allInfo.push(...additionalInfo);
}
}
// 处理第二种格式customerExpandFieldMap
if (formData.customerExpandFieldMap) {
const expandInfo = [];
const map = formData.customerExpandFieldMap;
Object.entries(map).forEach(([key, value]) => {
if (!map.hasOwnProperty(key)) return;
if (value && typeof value === 'object' && value.key) {
const question = value.key;
let answer = '';
if (value.typeCode === 'SINGLE_SELECT' || value.typeCode === 'MULTIPLE_SELECT') {
if (value.expandValueList && value.expandValueList.length > 0) {
answer = value.expandValueList.map(item => item.itemName).join('、');
}
} else if (value.typeCode === 'TEXT' || value.typeCode === 'TEXTAREA' || value.typeCode === 'NUMBER') {
answer = formData[key] || '';
}
if (answer && answer !== '暂无' && answer !== '') {
expandInfo.push(`${question}\n答案: ${answer}`);
}
}
});
if (expandInfo.length > 0) {
if (allInfo.length > 0) allInfo.push('\n');
allInfo.push('=== 问卷详细信息 ===');
allInfo.push(...expandInfo);
}
}
formInfoText = allInfo.length > 0 ? allInfo.join('\n') : '暂无表单信息';
} }
// ** 适配结束 **
// 构建聊天记录信息 // 构建聊天记录信息
const chatData = props.chatRecords || []; const chatData = props.chatRecords || [];
const chatInfoText = chatData.messages && chatData.messages.length > 0 ? const chatInfoText = chatData.messages && chatData.messages.length > 0 ?

View File

@@ -14,7 +14,7 @@
<div class="kpi-grid"> <div class="kpi-grid">
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.totalCalls }}</div> <div class="kpi-value">{{ props.kpiData.totalCalls }}</div>
<p>今日通话 <i class="info-icon" @mouseenter="showTooltip('totalCalls', $event)" @mouseleave="hideTooltip"></i></p> <p>本期通话 <i class="info-icon" @mouseenter="showTooltip('totalCalls', $event)" @mouseleave="hideTooltip"></i></p>
</div> </div>
<div class="kpi-item"> <div class="kpi-item">
<div class="kpi-value">{{ props.kpiData.successRate }}</div> <div class="kpi-value">{{ props.kpiData.successRate }}</div>
@@ -282,8 +282,8 @@ const tooltip = reactive({
// 指标说明配置 // 指标说明配置
const kpiDescriptions = { const kpiDescriptions = {
totalCalls: { totalCalls: {
title: '今日通话', title: '本期通话',
description: '今日总共通话的次数。' description: '本期总共通话的次数。'
}, },
successRate: { successRate: {
title: '电话接通率', title: '电话接通率',

View File

@@ -70,7 +70,7 @@
<span class="content-time">最新: {{ chatData.lastMessage }}</span> <span class="content-time">最新: {{ chatData.lastMessage }}</span>
</div> </div>
<div class="message-list"> <div class="message-list">
<div v-for="(message, index) in props.chatInfo.messages" :key="index" class="message-item"> <div v-for="(message, index) in props.chatInfo?.messages" :key="index" class="message-item">
<div class="message-header"> <div class="message-header">
<span class="message-sender">{{ message.format_direction }}</span> <span class="message-sender">{{ message.format_direction }}</span>
<span class="message-time">{{ message.format_add_time }}</span> <span class="message-time">{{ message.format_add_time }}</span>
@@ -142,8 +142,8 @@ const props = defineProps({
default: () => ({}) default: () => ({})
}, },
formInfo: { formInfo: {
type: Object, type: Array,
default: () => ({}) default: () => []
}, },
chatInfo: { chatInfo: {
type: Object, type: Object,
@@ -166,10 +166,25 @@ const chatMessages = computed(() => {
return props.chatInfo?.messages || [] return props.chatInfo?.messages || []
}) })
// 表单字段数据 // 表单字段数据 (已更新)
const formFields = computed(() => { const formFields = computed(() => {
const formData = props.formInfo const formData = props.formInfo
if (!formData || Object.keys(formData).length === 0) { console.log(8888888,formData)
// --- NEW LOGIC: 处理新的数组格式(问答列表) ---
if (Array.isArray(formData) && formData.length > 0) {
// 检查数组项结构,确保是新的问答格式
if (formData[0].question_label && formData[0].answer) {
// 直接将问答列表映射为 { label: question_label, value: answer } 格式
return formData.map(item => ({
label: item.question_label,
value: item.answer || '暂无回答'
}))
}
}
// --------------------------------------------------
// Fallback: 如果数据为空、null 或旧的空对象格式
if (!formData || (typeof formData === 'object' && Object.keys(formData).length === 0)) {
return [ return [
{ label: '姓名', value: '暂无数据' }, { label: '姓名', value: '暂无数据' },
{ label: '联系方式', value: '暂无数据' }, { label: '联系方式', value: '暂无数据' },
@@ -178,6 +193,7 @@ const formFields = computed(() => {
] ]
} }
// --- OLD LOGIC: 处理旧的对象格式(保持兼容性) ---
let fields = [] let fields = []
// 检查是否为第一种格式包含name, mobile等字段 // 检查是否为第一种格式包含name, mobile等字段
@@ -223,6 +239,7 @@ const formFields = computed(() => {
return allFields return allFields
}) })
// 聊天数据 // 聊天数据
const chatData = computed(() => ({ const chatData = computed(() => ({
count: props.chatInfo?.messages?.length || 0, count: props.chatInfo?.messages?.length || 0,

View File

@@ -432,7 +432,7 @@ const handleUnassignedRecordingsClick = async () => {
// 优先使用路由参数,其次是 Pinia store 中的用户信息,最后是备用值 // 优先使用路由参数,其次是 Pinia store 中的用户信息,最后是备用值
const user_name = routeParams.user_name || userStore.userInfo?.username || 'example_user'; const user_name = routeParams.user_name || userStore.userInfo?.username || 'example_user';
const response = await axios.post('https://mldash.nycjy.cn/api/v1/sales_timeline/get_sale_call_info', { const response = await axios.post('http://192.168.15.121:8890/api/v1/sales_timeline/get_sale_unassigned_call_info', {
user_name: user_name user_name: user_name
}); });
@@ -545,7 +545,26 @@ const totalCustomers = computed(() => {
return Math.max(...baseStages, 1); return Math.max(...baseStages, 1);
}); });
const getAttendedLessons = (classSituation, classNum) => {
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
const filtered = classNum.filter(n => n !== -1);
return filtered.length > 0 ? filtered.sort((a, b) => a - b).join(' ') : '未到课';
}
if (!classSituation) return '未到课';
if (Array.isArray(classSituation)) {
const filtered = classSituation.filter(i => i !== -1);
return filtered.length > 0 ? filtered.join(' ') : '未到课';
}
if (typeof classSituation === 'object') {
const lessons = Object.keys(classSituation).map(k => parseInt(k)).filter(n => !isNaN(n) && n !== -1).sort((a, b) => a - b);
return lessons.length > 0 ? lessons.join(' ') : '未到课';
}
return '未到课';
};
// **修改 getStageCount 函数**
const getStageCount = (stageType) => { const getStageCount = (stageType) => {
if (stageType === '成交') { if (stageType === '成交') {
return props.payMoneyCustomersCount || (props.payMoneyCustomersList?.length || 0); return props.payMoneyCustomersCount || (props.payMoneyCustomersList?.length || 0);
} }
@@ -558,31 +577,24 @@ const getStageCount = (stageType) => {
} }
return 0; return 0;
} }
const allCustomers = props.customersList || [];
// 筛选掉 '待加微' 的客户列表,只有这批客户才需要关注 '待填表单' 和 '待到课'
const nonPendingWechatCustomers = allCustomers.filter(c => c.type !== '待加微');
if (stageType === '待填表单') { if (stageType === '待填表单') {
if (props.customersList?.length) { // 对非 '待加微' 的客户进行筛选
return props.customersList.filter(customer => (customer.customer_occupation === '未知' || !customer.customer_occupation) || (customer.customer_child_education === '未知' || !customer.customer_child_education)).length; return nonPendingWechatCustomers.filter(customer => (customer.customer_occupation === '未知' || !customer.customer_occupation) || (customer.customer_child_education === '未知' || !customer.customer_child_education)).length;
}
return 0;
} }
if (stageType === '待到课') { if (stageType === '待到课') {
if (props.customersList?.length) { // 对非 '待加微' 的客户进行筛选,并使用 getAttendedLessons
return props.customersList.filter(customer => { return nonPendingWechatCustomers.filter(customer => getAttendedLessons(customer.class_situation, customer.class_num) === '未到课').length;
const classNum = customer.class_num;
const classSituation = customer.class_situation;
if (classNum && Array.isArray(classNum) && classNum.length > 0) return false;
if (!classSituation) return true;
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;
} }
if (props.customersList?.length) {
return props.customersList.filter(customer => customer.type === stageType).length; // 对于其他普通阶段(待加微、待入群、待联系),使用全部客户列表
if (allCustomers.length > 0) {
return allCustomers.filter(customer => customer.type === stageType).length;
} }
return props.data[stageType] || 0; return props.data[stageType] || 0;
}; };
@@ -615,6 +627,7 @@ const getPercentage = (count, stageId, stageName) => {
return Math.round((totalCustomers.value - count) / totalCustomers.value * 100); return Math.round((totalCustomers.value - count) / totalCustomers.value * 100);
}; };
// **修改 selectStage 函数**
const selectStage = (stageName) => { const selectStage = (stageName) => {
let filteredCustomers = []; let filteredCustomers = [];
if (stageName === '全部') { if (stageName === '全部') {
@@ -631,9 +644,13 @@ const selectStage = (stageName) => {
filteredCustomers = props.courseCustomers['课1-4'].filter(c => c.type === stageName); filteredCustomers = props.courseCustomers['课1-4'].filter(c => c.type === stageName);
} }
} else if (stageName === '待填表单') { } else if (stageName === '待填表单') {
filteredCustomers = props.customersList.filter(c => (c.customer_occupation === '未知' || !c.customer_occupation) || (c.customer_child_education === '未知' || !c.customer_child_education)); // 筛选掉 '待加微' 的客户列表
const customersToFilter = props.customersList.filter(c => c.type !== '待加微') || [];
filteredCustomers = customersToFilter.filter(c => (c.customer_occupation === '未知' || !c.customer_occupation) || (c.customer_child_education === '未知' || !c.customer_child_education));
} else if (stageName === '待到课') { } else if (stageName === '待到课') {
filteredCustomers = props.customersList.filter(c => getAttendedLessons(c.class_situation, c.class_num) === '未到课'); // 筛选掉 '待加微' 的客户列表
const customersToFilter = props.customersList.filter(c => c.type !== '待加微') || [];
filteredCustomers = customersToFilter.filter(c => getAttendedLessons(c.class_situation, c.class_num) === '未到课');
} else { } else {
filteredCustomers = props.customersList.filter(c => c.type === stageName); filteredCustomers = props.customersList.filter(c => c.type === stageName);
} }
@@ -679,23 +696,6 @@ const getCourseConversionRate = (courseNumber) => {
return Math.round((depositCount / courseStageCount) * 100); return Math.round((depositCount / courseStageCount) * 100);
}; };
const getAttendedLessons = (classSituation, classNum) => {
if (classNum && Array.isArray(classNum) && classNum.length > 0) {
const filtered = classNum.filter(n => n !== -1);
return filtered.length > 0 ? filtered.sort((a, b) => a - b).join(' ') : '未到课';
}
if (!classSituation) return '未到课';
if (Array.isArray(classSituation)) {
const filtered = classSituation.filter(i => i !== -1);
return filtered.length > 0 ? filtered.join(' ') : '未到课';
}
if (typeof classSituation === 'object') {
const lessons = Object.keys(classSituation).map(k => parseInt(k)).filter(n => !isNaN(n) && n !== -1).sort((a, b) => a - b);
return lessons.length > 0 ? lessons.join(' ') : '未到课';
}
return '未到课';
};
const getHealthClass = (health) => { const getHealthClass = (health) => {
if (!health && health !== 0) return ''; if (!health && health !== 0) return '';
const healthValue = parseFloat(health); const healthValue = parseFloat(health);
@@ -882,7 +882,7 @@ $indigo: #4f46e5;
font-weight: bold; font-weight: bold;
} }
.clickable { cursor: pointer; color: #1976d2; text-decoration: underline; &:hover { color: #1565c0; } } .clickable { cursor: pointer; color: #1976d2; text-decoration: underline; &:hover { color: #1565c0; } }
.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 $primary; } .message-number { font-weight: 600; color: $primary; } } .messages-list { display: flex; flex-direction: column; gap: 12px; .message-item { display: flex; gap: 8px; padding: 12px; background-color: #f9fafb; border-left: 3px solid $primary; } .message-number { font-weight: 600; color: $primary; } }
.course-sub-timeline { margin: 16px 0; padding: 12px; background-color: #f8fafc; border-radius: 8px; border: 1px solid #e2e8f0; } .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; } .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; } .course-timeline { background: white; border-radius: 6px; padding: 8px 12px; border: 1px solid #e5e7eb; }

View File

@@ -323,7 +323,7 @@ const payMoneyCustomersList = ref([]);
const payMoneyCustomersCount = ref(0); const payMoneyCustomersCount = ref(0);
// 表单信息 // 表单信息
const formInfo = ref({}); const formInfo = ref([]);
// 通话记录 // 通话记录
const callRecords = ref([]); const callRecords = ref([]);
// 聊天记录 // 聊天记录
@@ -369,7 +369,7 @@ async function getCoreKpi() {
// 今日通话数据 // 今日通话数据
const res = await getTodayCall(hasParams ? params : undefined) const res = await getTodayCall(hasParams ? params : undefined)
if (res.code === 200) { if (res.code === 200) {
kpiDataState.totalCalls = res.data.today_call kpiDataState.totalCalls = res.data.call_count
} }
// 转化率、分配数据量、加微率 // 转化率、分配数据量、加微率
const conversionRes = await getConversionRateAndAllocatedData(hasParams ? params : undefined) const conversionRes = await getConversionRateAndAllocatedData(hasParams ? params : undefined)
@@ -582,8 +582,9 @@ async function getCustomerForm() {
} }
try { try {
const res = await getCustomerFormInfo(params) const res = await getCustomerFormInfo(params)
console.log('获取客户表单数据:', res)
if(res.code === 200) { if(res.code === 200) {
formInfo.value = res.data formInfo.value = res.data[0].answers || []
} }
} catch (error) { } catch (error) {
// 静默处理错误 // 静默处理错误

View File

@@ -24,10 +24,10 @@
团队总业绩 团队总业绩
<span class="info-icon" @mouseenter="showTooltip($event, 'teamPerformance')" @mouseleave="hideTooltip"></span> <span class="info-icon" @mouseenter="showTooltip($event, 'teamPerformance')" @mouseleave="hideTooltip"></span>
</span> </span>
<span class="card-trend positive">{{ totalPerformance.team_current_vs_previous_deals }} vs 上期</span> <span class="card-trend positive">{{ totalPerformance.team_current_vs_previous_period_deals_comparison }} vs 上期</span>
</div> </div>
<div class="card-value">{{ totalPerformance.current_team_odd_numbers||0 }}</div> <div class="card-value">{{ totalPerformance.current_team_odd_numbers||0 }}</div>
<div class="card-subtitle">月目标完成率: {{ totalPerformance.team_monthly_performance }}</div> <div class="card-subtitle">月目标完成率: {{ totalPerformance.team_monthly_target_completion_rate }}</div>
</div> </div>
<div class="overview-card"> <div class="overview-card">
@@ -48,7 +48,7 @@
团队转化率 团队转化率
<span class="info-icon" @mouseenter="showTooltip($event, 'conversionRate')" @mouseleave="hideTooltip"></span> <span class="info-icon" @mouseenter="showTooltip($event, 'conversionRate')" @mouseleave="hideTooltip"></span>
</span> </span>
<span class="card-trend positive">{{ conversionRate.team_current_vs_previous_deals }} vs 上期</span> <span class="card-trend positive">{{ conversionRate.team_current_vs_previous_conversion_rate }} vs 上期</span>
</div> </div>
<div class="card-value">{{ conversionRate.center_conversion_rate }}</div> <div class="card-value">{{ conversionRate.center_conversion_rate }}</div>
<div class="card-subtitle">团队平均转化率: {{ conversionRate.average_conversion_rate }}</div> <div class="card-subtitle">团队平均转化率: {{ conversionRate.average_conversion_rate }}</div>

View File

@@ -60,42 +60,41 @@ import { ref, computed, onMounted, watch } from 'vue';
// 假设你有一个API服务来获取对比数据 // 假设你有一个API服务来获取对比数据
// import { getPerformanceComparisonData } from '@/api/senorManger.js'; // import { getPerformanceComparisonData } from '@/api/senorManger.js';
// 模拟API调用 // **MODIFIED**: 更新模拟API以返回新的数据结构
const getPerformanceComparisonData = async (params) => { const getPerformanceComparisonData = async (params) => {
console.log('模拟API请求:', params); console.log('模拟API请求 (新数据结构):', params);
return new Promise(resolve => { return new Promise(resolve => {
setTimeout(() => { setTimeout(() => {
// 模拟不同周期返回不同数据
let mockData; let mockData;
if (params.period === 'last_week') { if (params.period === 'last_week') {
mockData = { mockData = {
assignedLeads: 480, "allocated_data_volume": 650,
wechatAdds: 390, "wechat_added_volume": 410,
calls: 1450, "call_volume_after_classification": { "加微通话": 10, "20分钟通话": 50, "未分类": 150, "无效通话": 100, "促到课": 40 },
callDuration: 11500, "call_avg_duration_after_classification": { "加微通话": 4.5, "20分钟通话": 15.0, "未分类": 1.5, "无效通话": 1.0, "促到课": 2.0 },
deposits: 38, "deposit_volume": 40,
deals: 50, "transaction_volume": 52,
conversionRate: 10.4, "conversion_rate": "8.00%"
}; };
} else if (params.period === 'last_month') { } else if (params.period === 'last_month') {
mockData = { mockData = {
assignedLeads: 2000, "allocated_data_volume": 2800,
wechatAdds: 1500, "wechat_added_volume": 1800,
calls: 5800, "call_volume_after_classification": { "加微通话": 40, "20分钟通话": 220, "未分类": 600, "视频通话": 50, "无效通话": 450, "促到课": 180 },
callDuration: 48000, "call_avg_duration_after_classification": { "加微通话": 5.0, "20分钟通话": 16.0, "未分类": 1.8, "视频通话": 6.0, "无效通话": 1.1, "促到课": 1.5 },
deposits: 150, "deposit_volume": 180,
deals: 210, "transaction_volume": 230,
conversionRate: 10.5, "conversion_rate": "8.21%"
}; };
} else { } else { // last_quarter
mockData = { mockData = {
assignedLeads: 6500, "allocated_data_volume": 8500,
wechatAdds: 5200, "wechat_added_volume": 5500,
calls: 18000, "call_volume_after_classification": { "加微通话": 120, "20分钟通话": 650, "未分类": 1800, "视频通话": 150, "无效通话": 1200, "促到课": 500 },
callDuration: 150000, "call_avg_duration_after_classification": { "加微通话": 4.8, "20分钟通话": 16.5, "未分类": 1.7, "视频通话": 5.5, "无效通话": 1.0, "促到课": 1.8 },
deposits: 450, "deposit_volume": 550,
deals: 600, "transaction_volume": 700,
conversionRate: 9.2, "conversion_rate": "8.24%"
}; };
} }
resolve({ code: 200, data: mockData }); resolve({ code: 200, data: mockData });
@@ -125,12 +124,11 @@ const selectedPeriodLabel = computed(() => periodLabels[selectedPeriod.value]);
const fetchComparisonData = async () => { const fetchComparisonData = async () => {
isLoading.value = true; isLoading.value = true;
try { try {
// 真实场景中这里应该调用API
const res = await getPerformanceComparisonData({ period: selectedPeriod.value }); const res = await getPerformanceComparisonData({ period: selectedPeriod.value });
if (res.code === 200) { if (res.code === 200) {
previousPeriodData.value = res.data; previousPeriodData.value = res.data;
} else { } else {
previousPeriodData.value = null; // API出错或无数据 previousPeriodData.value = null;
} }
} catch (error) { } catch (error) {
console.error('获取对比数据失败:', error); console.error('获取对比数据失败:', error);
@@ -144,14 +142,54 @@ onMounted(() => {
fetchComparisonData(); fetchComparisonData();
}); });
// 如果本期数据可能变化可以监听props来刷新
watch(() => props.currentPeriodData, () => { watch(() => props.currentPeriodData, () => {
fetchComparisonData(); fetchComparisonData();
}, { deep: true }); }, { deep: true });
// **NEW**: 新增一个工具函数用于处理API返回的复杂数据结构
// 并将其转换为表格渲染所需的扁平化结构
const processRawData = (rawData) => {
if (!rawData) return null;
// 1. 计算通话总量
const totalCalls = Object.values(rawData.call_volume_after_classification || {})
.reduce((sum, count) => sum + count, 0);
// 2. 计算通话总时长 (分钟)
// 逻辑: (分类通话数 * 分类平均时长) 的总和
const callVolumes = rawData.call_volume_after_classification || {};
const avgDurations = rawData.call_avg_duration_after_classification || {};
const totalDurationInMinutes = Object.keys(callVolumes).reduce((sum, key) => {
const volume = callVolumes[key] || 0;
const avgDuration = avgDurations[key] || 0;
return sum + (volume * avgDuration);
}, 0);
// 3. 将转化率字符串 "7.98%" 转为数字 7.98
const conversionRateValue = parseFloat(rawData.conversion_rate) || 0;
// 4. 返回一个与旧版组件兼容的对象结构
return {
assignedLeads: rawData.allocated_data_volume,
wechatAdds: rawData.wechat_added_volume,
calls: totalCalls,
// 组件内部格式化时会除以60因此这里需要乘以60将分钟转为秒
callDuration: totalDurationInMinutes * 60,
deposits: rawData.deposit_volume,
deals: rawData.transaction_volume,
conversionRate: conversionRateValue,
};
};
const comparedMetrics = computed(() => { const comparedMetrics = computed(() => {
if (!props.currentPeriodData || !previousPeriodData.value) return []; // **MODIFIED**: 在计算前,先使用 processRawData 对数据进行处理
const processedCurrentData = processRawData(props.currentPeriodData);
const processedPreviousData = processRawData(previousPeriodData.value);
if (!processedCurrentData || !processedPreviousData) return [];
// 指标配置保持不变,因为数据已经被处理成它期望的格式
const metricsConfig = [ const metricsConfig = [
{ key: 'assignedLeads', label: '分配数据量', unit: '个' }, { key: 'assignedLeads', label: '分配数据量', unit: '个' },
{ key: 'wechatAdds', label: '加微量', unit: '个' }, { key: 'wechatAdds', label: '加微量', unit: '个' },
@@ -163,8 +201,8 @@ const comparedMetrics = computed(() => {
]; ];
return metricsConfig.map(metric => { return metricsConfig.map(metric => {
const current = props.currentPeriodData[metric.key]; const current = processedCurrentData[metric.key];
const previous = previousPeriodData.value[metric.key]; const previous = processedPreviousData[metric.key];
return { return {
...metric, ...metric,
current, current,
@@ -198,6 +236,7 @@ const calculateChange = (current, previous) => {
const formatValue = (value, unit) => { const formatValue = (value, unit) => {
if (value === null || value === undefined) return '-'; if (value === null || value === undefined) return '-';
if (unit === '分钟') { if (unit === '分钟') {
// 假设传入的value是秒转换为分钟显示
return `${Math.round(value / 60)} 分钟`; return `${Math.round(value / 60)} 分钟`;
} }
if (unit === '%') { if (unit === '%') {
@@ -212,6 +251,7 @@ const formatChange = (diff, unit) => {
const absDiff = Math.abs(diff); const absDiff = Math.abs(diff);
if (unit === '分钟') { if (unit === '分钟') {
// 假设diff是秒转换为分钟显示
return `${prefix}${Math.round(absDiff / 60)} 分钟`; return `${prefix}${Math.round(absDiff / 60)} 分钟`;
} }
if (unit === '%') { if (unit === '%') {
@@ -229,12 +269,12 @@ const getChangeClass = (trend) => {
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
/* 样式部分无需改动,因此省略以保持简洁 */
.performance-comparison { .performance-comparison {
background: #ffffff; background: #ffffff;
border-radius: 12px; border-radius: 12px;
padding: 2rem; padding: 2rem;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05);
margin-top: 1.5rem;
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
} }
@@ -242,7 +282,6 @@ const getChangeClass = (trend) => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
margin-bottom: 2rem;
padding-bottom: 1rem; padding-bottom: 1rem;
border-bottom: 1px solid #f1f5f9; border-bottom: 1px solid #f1f5f9;

View File

@@ -64,6 +64,10 @@
:severeTimeoutRate="statisticalIndicators.severeTimeoutRate" :severeTimeoutRate="statisticalIndicators.severeTimeoutRate"
:formCompletionRate="statisticalIndicators.formCompletionRate" :formCompletionRate="statisticalIndicators.formCompletionRate"
/> />
<!-- 新增业绩周期对比组件 -->
<div v-if="false" class="performance-comparison-section">
<PerformanceComparison :current-period-data="currentPeriodMetrics" />
</div>
<!-- Bottom Section --> <!-- Bottom Section -->
<div class="bottom-section"> <div class="bottom-section">
<!-- Left Section - Group Performance Ranking --> <!-- Left Section - Group Performance Ranking -->
@@ -90,10 +94,7 @@
</div> </div>
</div> </div>
<!-- 新增业绩周期对比组件 -->
<div v-if="cardVisibility.performanceComparison" class="performance-comparison-section">
<PerformanceComparison :current-period-data="currentPeriodMetrics" />
</div>
<!-- Team Members Detail Section --> <!-- Team Members Detail Section -->
<div class="team-detail-section" v-if="selectedGroup && cardVisibility.teamDetail"> <div class="team-detail-section" v-if="selectedGroup && cardVisibility.teamDetail">
@@ -996,10 +997,6 @@ const hideTooltip = () => {
overflow: auto; overflow: auto;
} }
/* 新增 */
.performance-comparison-section {
margin-top: 1rem;
}
.action-items-compact { .action-items-compact {
overflow: hidden; overflow: hidden;