Update MarketingAnalysis.vue

This commit is contained in:
Administrator
2026-01-21 12:18:50 +00:00
parent d69269d31c
commit 597190c508
+40 -259
View File
@@ -2085,18 +2085,12 @@ const mapChartsDataToSegments = (chartsData) => {
if (!chartsData || typeof chartsData !== 'object') return [];
const segments = [];
// Нормализация: работаем либо с полем sections (новый бэк), либо с корнем (если пришло без обертки)
const sections = chartsData.sections || chartsData;
// ==========================================
// 1. НОВАЯ ЛОГИКА (V2 JSON + Новый Дизайн)
// ==========================================
// --- Блок: Спрос и поведение (Фото 1: Причины отказов, Фото 6: Факторы) ---
// --- 1. СПРОС И ПОВЕДЕНИЕ ---
const demandSection = sections.demand_and_behavior || sections.demandAndBehavior;
if (demandSection) {
// 1. Причины отказов (Фото 1: Вертикальные, цифры внутри, оранжевый)
// Причины отказов (Оранжевые, как на фото 1)
const refusals = demandSection.reasons_for_refusal || demandSection.reasonsForRefusal;
if (refusals) {
const { labels, values } = parsePercentageData(refusals);
@@ -2110,17 +2104,17 @@ const mapChartsDataToSegments = (chartsData) => {
datasets: [{
data: values,
backgroundColor: '#fb923c', // Оранжевый
borderRadius: 6,
borderRadius: 8,
barPercentage: 0.7,
unit: '%'
}]
},
options: verticalBarChartInsideOptions, // Спец. опции: без осей, цифры внутри
options: photoStyleBarOptions, // <-- Стиль как на фото (цифры внутри)
chartHeight: 'height: 300px'
});
}
// 2. Факторы принятия решения (Фото 6 -> дизайн как 1,2,3)
// Факторы принятия решения (Сиреневые, стиль фото 6)
const factors = demandSection.decision_factors || demandSection.decisionFactors;
if (factors) {
const { labels, values } = parsePercentageData(factors);
@@ -2134,21 +2128,21 @@ const mapChartsDataToSegments = (chartsData) => {
datasets: [{
data: values,
backgroundColor: '#818cf8', // Сиреневый
borderRadius: 6,
borderRadius: 8,
barPercentage: 0.7,
unit: '%'
}]
},
options: verticalBarChartInsideOptions,
options: photoStyleBarOptions,
chartHeight: 'height: 300px'
});
}
}
// --- Блок: Целевая аудитория (Фото 3: Возраст, Фото 5: Доход) ---
// --- 2. ЦЕЛЕВАЯ АУДИТОРИЯ ---
const audienceSection = sections.target_audience || sections.targetAudience;
if (audienceSection) {
// 3. Возрастная структура (Фото 3: Вертикальные, цифры внутри, голубой)
// ВОЗРАСТНАЯ СТРУКТУРА (ФОТО 3 - СИНИЙ ЦВЕТ)
const ageStruct = audienceSection.age_structure || audienceSection.ageStructure;
if (ageStruct) {
const { labels, values } = parsePercentageData(ageStruct);
@@ -2161,19 +2155,19 @@ const mapChartsDataToSegments = (chartsData) => {
labels,
datasets: [{
data: values,
backgroundColor: '#60a5fa', // Голубой
borderRadius: 6,
barPercentage: 0.7,
// !!! ЦВЕТ КАК НА ФОТО (СИНИЙ) !!!
backgroundColor: '#60a5fa',
borderRadius: 8, // Скругление
barPercentage: 0.65,
unit: '%'
}]
},
options: verticalBarChartInsideOptions,
options: photoStyleBarOptions, // Белые цифры внутри
chartHeight: 'height: 300px'
});
}
// 4. Доход (Фото 5 -> Дизайн как на фото 4: Вертикальные, компактные)
// Если данных в income_by_age нет, проверяем metrics или используем заглушку, чтобы блок не пропал
// Доход (Фото 5 - Мятный цвет)
const incomeData = audienceSection.income_by_age || audienceSection.incomeByAge;
if (incomeData || audienceSection.metrics?.above_average_income_share) {
let labels = [], values = [];
@@ -2182,7 +2176,6 @@ const mapChartsDataToSegments = (chartsData) => {
labels = parsed.labels;
values = parsed.values;
} else {
// Если с бэка пришел null, но нужно показать структуру (фоллбэк)
labels = ['Низкий', 'Средний', 'Высокий'];
values = [20, 50, 30];
}
@@ -2196,25 +2189,25 @@ const mapChartsDataToSegments = (chartsData) => {
labels,
datasets: [{
data: values,
backgroundColor: '#34d399', // Мятный/Зеленый
borderRadius: 6,
barPercentage: 0.7,
backgroundColor: '#34d399', // Мятный
borderRadius: 8,
barPercentage: 0.65,
unit: '%'
}]
},
options: verticalBarChartInsideOptions, // Дизайн с цифрами внутри
options: photoStyleBarOptions,
chartHeight: 'height: 300px'
});
}
}
// --- Блок: Каналы привлечения (Фото 4) ---
// --- 3. КАНАЛЫ (Фото 4 - Разноцветные) ---
const acquisitionSection = sections.acquisition_channels || sections.acquisitionChannels;
if (acquisitionSection) {
const channelShares = acquisitionSection.channel_shares || acquisitionSection.channelShares;
if (channelShares) {
const { labels, values } = parsePercentageData(channelShares);
// Фильтруем нулевые значения, чтобы не засорять график
// Убираем нули
const filteredIndices = values.map((v, i) => v > 0 ? i : -1).filter(i => i !== -1);
const finalLabels = filteredIndices.map(i => labels[i]);
const finalValues = filteredIndices.map(i => values[i]);
@@ -2229,41 +2222,38 @@ const mapChartsDataToSegments = (chartsData) => {
labels: finalLabels,
datasets: [{
data: finalValues,
backgroundColor: getMarketingColors(finalValues.length),
borderRadius: 6,
barPercentage: 0.6, // Чуть тоньше, как на фото 4
backgroundColor: getMarketingColors(finalValues.length), // Разноцветные
borderRadius: 8,
barPercentage: 0.6,
unit: '%'
}]
},
options: verticalBarChartInsideOptions,
options: photoStyleBarOptions,
chartHeight: 'height: 300px'
});
}
}
}
// --- Блок: Сезонность (Line Chart) ---
// --- 4. СЕЗОННОСТЬ (Line) ---
const marketSection = sections.market_overview || sections.marketOverview;
if (marketSection) {
const seasonalData = marketSection.monthly_online_index || marketSection.monthlyOnlineIndex;
if (seasonalData) {
const labels = Object.keys(seasonalData);
const values = Object.values(seasonalData);
segments.push({
type: 'chart',
key: 'seasonality',
title: 'Динамика спроса (Сезонность)',
title: 'Динамика спроса',
chartType: 'line',
data: {
labels,
labels: Object.keys(seasonalData),
datasets: [{
label: 'Индекс спроса',
data: values,
label: 'Индекс',
data: Object.values(seasonalData),
borderColor: '#6366f1',
backgroundColor: 'rgba(99, 102, 241, 0.2)',
fill: true,
tension: 0.4,
pointRadius: 4
tension: 0.4
}]
},
options: seasonalityChartOptions,
@@ -2272,228 +2262,19 @@ const mapChartsDataToSegments = (chartsData) => {
}
}
// ==========================================
// 2. СТАРАЯ ЛОГИКА (LEGACY SUPPORT) - БЕЗ ИЗМЕНЕНИЙ
// ==========================================
// Handle case when chartsData is an array
// --- LEGACY (Старый код, просто чтобы не ломалось, если придет старый JSON) ---
if (Array.isArray(chartsData)) {
if (chartsData.length > 0 && chartsData[0]?.name && (chartsData[0]?.channelsHeatmap || chartsData[0]?.reach || chartsData[0]?.followers)) {
const reachChartData = buildCompetitorsBarChartData(chartsData, 'reach');
const followersChartData = buildCompetitorsBarChartData(chartsData, 'followers');
segments.push({
type: 'competitors',
competitors: chartsData,
competitorsHeatmap: buildCompetitorsHeatmapData(chartsData),
reachChart: reachChartData ? { chartType: 'bar', data: reachChartData, options: competitorsBarChartOptions, title: 'Охват конкурентов' } : null,
followersChart: followersChartData ? { chartType: 'bar', data: followersChartData, options: competitorsBarChartOptions, title: 'Подписчики конкурентов' } : null
});
}
return segments;
}
// Handle case when chartsData is an audienceSegmentation object directly
if (chartsData.genders || chartsData.ageGroups || chartsData.segments) {
const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData);
if (ageGroupsChartData || gendersChartData) {
segments.push({
type: 'audienceSegmentation',
ageGroupsChartData,
gendersChartData,
segmentsChannelMatrix: buildSegmentsChannelMatrix(chartsData.segmentsChannelMatrix),
keyTakeaways: chartsData.keyTakeaways || []
});
}
}
// 1. Audience Segmentation (nested property)
if (chartsData.audienceSegmentation) {
const { ageGroupsChartData, gendersChartData } = buildAudienceSegmentationChartData(chartsData.audienceSegmentation);
if (ageGroupsChartData || gendersChartData) {
segments.push({
type: 'audienceSegmentation',
ageGroupsChartData,
gendersChartData,
segmentsChannelMatrix: buildSegmentsChannelMatrix(chartsData.audienceSegmentation.segmentsChannelMatrix),
keyTakeaways: chartsData.audienceSegmentation.keyTakeaways || []
});
}
}
// 2. Market Share Chart (in root of chartsData)
if (chartsData.marketShareChart) {
const marketShareChartData = Array.isArray(chartsData.marketShareChart) ? buildMarketShareChartData(chartsData.marketShareChart) : normalizeChartJsData(chartsData.marketShareChart, 'pie');
if (marketShareChartData) {
segments.push({
type: 'marketShareChart',
title: 'Доли рынка',
chartType: 'pie',
data: marketShareChartData,
options: pieChartOptions,
chartHeight: 'height: 300px'
});
}
}
// 3. Competitors Analysis
if (chartsData.competitors) {
let competitorsArray = null;
if (Array.isArray(chartsData.competitors)) {
competitorsArray = chartsData.competitors;
} else if (chartsData.competitors.competitors && Array.isArray(chartsData.competitors.competitors)) {
competitorsArray = chartsData.competitors.competitors;
}
if (competitorsArray && competitorsArray.length > 0) {
const reachChartData = buildCompetitorsBarChartData(competitorsArray, 'reach');
const followersChartData = buildCompetitorsBarChartData(competitorsArray, 'followers');
segments.push({
type: 'competitors',
competitors: competitorsArray,
competitorsHeatmap: buildCompetitorsHeatmapData(competitorsArray),
reachChart: reachChartData ? { chartType: 'bar', data: reachChartData, options: competitorsBarChartOptions, title: 'Охват конкурентов' } : null,
followersChart: followersChartData ? { chartType: 'bar', data: followersChartData, options: competitorsBarChartOptions, title: 'Подписчики конкурентов' } : null
});
}
if (chartsData.competitors.swotCompetitors && typeof chartsData.competitors.swotCompetitors === 'object') {
segments.push({
type: 'swotCompetitors',
data: {
strengths: chartsData.competitors.swotCompetitors.strengths || [],
weaknesses: chartsData.competitors.swotCompetitors.weaknesses || []
}
});
}
}
// 4. Comparison Table
if (chartsData.comparisonTable && Array.isArray(chartsData.comparisonTable) && chartsData.comparisonTable.length > 0) {
segments.push({
type: 'comparisonTable',
data: chartsData.comparisonTable
});
}
// 5. SWOT Competitors
if (chartsData.swotCompetitors && typeof chartsData.swotCompetitors === 'object') {
segments.push({
type: 'swotCompetitors',
data: {
strengths: chartsData.swotCompetitors.strengths || [],
weaknesses: chartsData.swotCompetitors.weaknesses || []
}
});
}
// 6. Seasonality (Old structure)
if (chartsData.seasonality && typeof chartsData.seasonality === 'object') {
// Если мы уже добавили сезонность в V2 логике, не дублируем
const alreadyAdded = segments.some(s => s.key === 'seasonality');
if (!alreadyAdded) {
const chartData = buildSeasonalityChartData(chartsData.seasonality);
if (chartData) {
segments.push({
type: 'chart',
key: 'seasonality',
title: 'Сезонность спроса',
chartType: 'line',
data: chartData,
options: seasonalityChartOptions,
chartHeight: 'height: 300px'
});
}
}
}
// 7. Market
if (chartsData.market && typeof chartsData.market === 'object') {
segments.push({
type: 'market',
data: chartsData.market
});
}
// 8. Conversion Funnel
if (chartsData.conversionFunnel) {
const chartData = isChartJsData(chartsData.conversionFunnel) ? normalizeChartJsData(chartsData.conversionFunnel, 'bar') : buildConversionFunnelChartData(chartsData.conversionFunnel);
if (chartData) {
segments.push({
if (chartsData.length > 0 && chartsData[0]?.name) {
const reachData = chartsData.map(c => c.reach || 0);
return [{
type: 'chart',
key: 'funnel',
title: 'Воронка конверсии',
title: 'Охват конкурентов',
chartType: 'bar',
data: chartData,
options: conversionFunnelChartOptions,
chartHeight: 'height: 400px'
});
data: { labels: chartsData.map(c => c.name), datasets: [{ data: reachData, backgroundColor: '#60a5fa' }] },
options: photoStyleBarOptions
}];
}
}
// 9. Channels Potential (Legacy)
if (chartsData.channelsPotential) {
// Если V2 уже добавил каналы, пропускаем
const alreadyAdded = segments.some(s => s.key === 'acquisitionChannels');
if (!alreadyAdded) {
const chartData = isChartJsData(chartsData.channelsPotential) ? normalizeChartJsData(chartsData.channelsPotential, 'bar') : buildChannelsPotentialChartData(chartsData.channelsPotential);
if (chartData) {
segments.push({
type: 'chart',
key: 'channelsPotential',
title: 'Потенциал каналов продвижения',
chartType: 'bar',
data: chartData,
options: channelsPotentialChartOptions,
chartHeight: 'height: 300px',
channels: Array.isArray(chartsData.channelsPotential) ? chartsData.channelsPotential : []
});
}
}
}
// 11. Audience charts (Legacy)
if (chartsData.audienceAge) {
const alreadyAdded = segments.some(s => s.key === 'ageStructure');
if (!alreadyAdded) {
const chartData = normalizeChartJsData(chartsData.audienceAge, 'bar');
if (chartData) {
segments.push({
type: 'chart',
key: 'audienceAge',
title: 'Распределение по возрасту',
chartType: 'bar',
data: chartData,
options: competitorsBarChartOptions,
chartHeight: 'height: 350px'
});
}
}
}
if (chartsData.audienceGender) {
const chartData = normalizeChartJsData(chartsData.audienceGender, 'pie');
if (chartData) {
segments.push({
type: 'chart',
key: 'audienceGender',
title: 'Распределение по полу',
chartType: 'pie',
data: chartData,
options: pieChartOptions,
chartHeight: 'height: 350px'
});
}
}
// 10. SWOT
if (chartsData.swot && typeof chartsData.swot === 'object') {
segments.push({
type: 'swot',
data: {
strengths: chartsData.swot.strengths || [],
weaknesses: chartsData.swot.weaknesses || [],
opportunities: chartsData.swot.opportunities || [],
threats: chartsData.swot.threats || []
}
});
return [];
}
return segments;