From c38c7258aa4d3b81d00327248f7eb9dbcad481d8 Mon Sep 17 00:00:00 2001 From: Administrator Date: Thu, 22 Jan 2026 08:00:26 +0000 Subject: [PATCH 01/17] Update 2 files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда: Генерация изображений для постов.md - /API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md --- ...окументация для фронтенда Генерация изображений для постов.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename API Документация: Генерация стратегии продвижения (Frontend/{Документация для фронтенда: Генерация изображений для постов.md => Документация для фронтенда Генерация изображений для постов.md} (100%) diff --git a/API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда: Генерация изображений для постов.md b/API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md similarity index 100% rename from API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда: Генерация изображений для постов.md rename to API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md From 1ee10a64a5d056e107d981c7006aaa465fd285aa Mon Sep 17 00:00:00 2001 From: Administrator Date: Thu, 22 Jan 2026 08:02:29 +0000 Subject: [PATCH 02/17] Update 2 files MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md - /API Документация Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md --- ...Документация для фронтенда Генерация изображений для постов.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {API Документация: Генерация стратегии продвижения (Frontend => API Документация Генерация стратегии продвижения (Frontend}/Документация для фронтенда Генерация изображений для постов.md (100%) diff --git a/API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md b/API Документация Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md similarity index 100% rename from API Документация: Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md rename to API Документация Генерация стратегии продвижения (Frontend/Документация для фронтенда Генерация изображений для постов.md From 112f470cc377d5be517b0f1942fdd8e81699fcde Mon Sep 17 00:00:00 2001 From: arys Date: Thu, 22 Jan 2026 13:36:32 +0500 Subject: [PATCH 03/17] fix --- src/views/pages/MarketingAnalysis.vue | 165 ++++++++++++++------------ 1 file changed, 86 insertions(+), 79 deletions(-) diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue index a586411..542fe95 100644 --- a/src/views/pages/MarketingAnalysis.vue +++ b/src/views/pages/MarketingAnalysis.vue @@ -301,7 +301,7 @@
-
Арыстанбек
+
Возрастная структура
-
-
-
-
Возраст
-
Доля
-
-
-
-
{{ row.label }}
-
{{ row.value }}%
-
-
-
-
@@ -338,20 +324,6 @@
-
-
-
-
Возраст
-
Доход
-
-
-
-
{{ row.label }}
-
{{ row.value }} ₸
-
-
-
-
@@ -414,20 +386,6 @@
-
-
-
-
Конкурент
-
Видимость
-
-
-
-
{{ row.label }}
-
{{ row.visibility ?? '—' }}
-
-
-
-
@@ -731,20 +689,6 @@
-
-
-
-
Канал
-
Доля
-
-
-
-
{{ row.label }}
-
{{ row.value }}%
-
-
-
-
@@ -1418,10 +1362,63 @@ const ageRows = computed(() => { const compactBarOptions = computed(() => ({ maintainAspectRatio: false, - plugins: { legend: { display: false } }, + plugins: { + legend: { display: false }, + barValues: { + id: 'barValues', + afterDatasetsDraw(chart) { + const { ctx, data } = chart; + ctx.save(); + ctx.font = 'bold 12px Inter, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + + data.datasets.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i); + if (!meta.hidden) { + meta.data.forEach((bar, index) => { + const value = dataset.data[index]; + if (value !== null && value !== undefined && value !== 0) { + const unit = dataset.unit || ''; + const label = value.toLocaleString() + unit; + const isHorizontal = chart.options.indexAxis === 'y'; + + if (isHorizontal) { + const barWidth = Math.abs(bar.x - bar.base); + if (barWidth > 40) { + ctx.fillStyle = '#ffffff'; + ctx.textAlign = 'right'; + ctx.fillText(label, bar.x - 10, bar.y); + } else { + ctx.fillStyle = '#64748b'; + ctx.textAlign = 'left'; + ctx.fillText(label, bar.x + 10, bar.y); + } + } else { + const barHeight = Math.abs(bar.base - bar.y); + ctx.textAlign = 'center'; + if (barHeight > 25) { + ctx.fillStyle = '#ffffff'; + ctx.fillText(label, bar.x, bar.y + 14); + } else { + ctx.fillStyle = '#64748b'; + ctx.fillText(label, bar.x, bar.y - 10); + } + } + } + }); + } + }); + ctx.restore(); + } + } + }, scales: { - x: { ticks: { color: '#64748B' }, grid: { display: false, drawBorder: false } }, - y: { ticks: { color: '#64748B' }, grid: { color: 'rgba(148,163,184,0.25)', drawBorder: false } } + x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { display: false, drawBorder: false } }, + y: { + ticks: { color: '#64748B', font: { size: 10 } }, + grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false }, + beginAtZero: true + } } })); @@ -1507,6 +1504,7 @@ const ageBarData = computed(() => ({ datasets: [ { data: ageRows.value.map((r) => r.value), + unit: '%', borderRadius: 10, backgroundColor: (context) => { const { chart } = context; @@ -1521,7 +1519,7 @@ const ageBarData = computed(() => ({ // Income by age rows const incomeRows = computed(() => { const incomeByAge = report.value?.sections?.target_audience?.income_by_age ?? {}; - + // Маппинг ключей бэкенда на ключи фронтенда const backendKeyMap = { '18-24': { '18-24': 1.0 }, @@ -1573,6 +1571,7 @@ const incomeBarData = computed(() => ({ datasets: [ { data: incomeRows.value.map((r) => r.value), + unit: ' ₸', borderRadius: 10, backgroundColor: (context) => { const { chart } = context; @@ -1683,6 +1682,7 @@ const visibilityBarData = computed(() => { datasets: [ { data: values, + unit: '%', borderRadius: 12, borderSkipped: false, backgroundColor: (context) => { @@ -1697,15 +1697,17 @@ const visibilityBarData = computed(() => { }; }); -const visibilityBarOptions = computed(() => ({ - indexAxis: 'y', - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(148,163,184,0.25)', drawBorder: false } }, - y: { ticks: { color: '#64748B' }, grid: { display: false, drawBorder: false } } - } -})); +const visibilityBarOptions = computed(() => { + const base = compactBarOptions.value; + return { + ...base, + indexAxis: 'y', + scales: { + x: { ...base.scales.x, grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false } }, + y: { ...base.scales.y, grid: { display: false, drawBorder: false } } + } + }; +}); const pptFunnelSteps = computed(() => { const fm = report.value?.sections?.customer_journey?.funnel_metrics ?? {}; @@ -1825,6 +1827,7 @@ const decisionFactorsBarData = computed(() => { datasets: [ { data: values, + unit: '%', borderRadius: 10, backgroundColor: (context) => { const { chart } = context; @@ -1858,6 +1861,7 @@ const refusalReasonsBarData = computed(() => { datasets: [ { data: values, + unit: '%', borderRadius: 10, backgroundColor: (context) => { const { chart } = context; @@ -1893,6 +1897,7 @@ const channelSharesBarData = computed(() => { datasets: [ { data: items.map((i) => percentToNumber(i.value)), + unit: '%', borderRadius: 12, borderSkipped: false, backgroundColor: (context) => { @@ -1917,15 +1922,17 @@ const channelSharesTableRows = computed(() => { ]; }); -const channelSharesBarOptions = computed(() => ({ - indexAxis: 'y', - maintainAspectRatio: false, - plugins: { legend: { display: false } }, - scales: { - x: { ticks: { color: '#64748B' }, grid: { color: 'rgba(148,163,184,0.25)', drawBorder: false } }, - y: { ticks: { color: '#64748B' }, grid: { display: false, drawBorder: false } } - } -})); +const channelSharesBarOptions = computed(() => { + const base = compactBarOptions.value; + return { + ...base, + indexAxis: 'y', + scales: { + x: { ...base.scales.x, grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false } }, + y: { ...base.scales.y, grid: { display: false, drawBorder: false } } + } + }; +}); const channelEfficiencyRows = computed(() => { const eff = report.value?.sections?.acquisition_channels?.channel_efficiency ?? {}; From a4bd0cd39d46f21c98a9e8e085c37d02691bbb55 Mon Sep 17 00:00:00 2001 From: arys Date: Thu, 22 Jan 2026 15:49:18 +0500 Subject: [PATCH 04/17] fix --- src/views/pages/MarketingAnalysis.vue | 133 +++--------------- .../pages/marketing/MarketingAnalysis.vue | 34 ++--- 2 files changed, 38 insertions(+), 129 deletions(-) diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue index 542fe95..a2b5572 100644 --- a/src/views/pages/MarketingAnalysis.vue +++ b/src/views/pages/MarketingAnalysis.vue @@ -266,39 +266,9 @@ />
-
-
-
-
Сегменты аудитории для рекламы
- -
- -
-
-
Сегмент
-
Доля
-
-
-
-
-
{{ seg.name }}
-
{{ seg.value }}%
-
-
-
-
-
-
-
-
-
- -
+
-
+
Возрастная структура
@@ -312,7 +282,7 @@
-
+
Доход и платёжеспособность
@@ -481,20 +451,6 @@
-
-
-
-
Тип запроса
-
Доля
-
-
-
-
{{ row.label }}
-
{{ row.value }}%
-
-
-
-
@@ -511,20 +467,6 @@
-
-
-
-
Фактор
-
Доля
-
-
-
-
{{ row.label }}
-
{{ row.value }}%
-
-
-
-
@@ -539,20 +481,6 @@
-
-
-
-
Причина
-
Доля
-
-
-
-
{{ row.label }}
-
{{ row.value }}%
-
-
-
-
@@ -677,7 +605,7 @@ /> -
+
Доли каналов
@@ -691,31 +619,6 @@
-
-
-
-
Стоимость привлечения
- -
-
-
-
Канал
-
Стоимость
-
Эффективность
-
-
-
-
{{ row.label }}
-
{{ row.cost }}
-
{{ row.status }}
-
-
-
-
-
@@ -1165,6 +1068,8 @@ function resetDashboard() { const PPT_BLUE = '#5B89FF'; const PPT_ORANGE = '#FF8E4F'; const PPT_GREEN = '#52C41A'; +const PPT_GRADIENT_BLUE_START = '#5B89FF'; +const PPT_GRADIENT_BLUE_END = '#82A7FF'; const roiValue = computed(() => report.value?.sections?.marketing_economics?.metrics?.roi ?? '—'); const cacValue = computed(() => report.value?.sections?.marketing_economics?.metrics?.customer_acquisition_cost_cac ?? '—'); @@ -1386,10 +1291,12 @@ const compactBarOptions = computed(() => ({ const barWidth = Math.abs(bar.x - bar.base); if (barWidth > 40) { ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 12px Inter, system-ui, sans-serif'; ctx.textAlign = 'right'; - ctx.fillText(label, bar.x - 10, bar.y); + ctx.fillText(label, bar.x - 12, bar.y); } else { ctx.fillStyle = '#64748b'; + ctx.font = 'bold 11px Inter, system-ui, sans-serif'; ctx.textAlign = 'left'; ctx.fillText(label, bar.x + 10, bar.y); } @@ -1398,10 +1305,12 @@ const compactBarOptions = computed(() => ({ ctx.textAlign = 'center'; if (barHeight > 25) { ctx.fillStyle = '#ffffff'; - ctx.fillText(label, bar.x, bar.y + 14); + ctx.font = 'bold 14px Inter, system-ui, sans-serif'; + ctx.fillText(label, bar.x, bar.y + 16); } else { ctx.fillStyle = '#64748b'; - ctx.fillText(label, bar.x, bar.y - 10); + ctx.font = 'bold 12px Inter, system-ui, sans-serif'; + ctx.fillText(label, bar.x, bar.y - 12); } } } @@ -1509,8 +1418,8 @@ const ageBarData = computed(() => ({ backgroundColor: (context) => { const { chart } = context; const { ctx, chartArea } = chart; - if (!chartArea) return 'rgba(91,137,255,0.55)'; - return makeVerticalGradient(ctx, chartArea, 'rgba(91,137,255,0.85)', 'rgba(91,137,255,0.25)'); + if (!chartArea) return PPT_BLUE; + return makeVerticalGradient(ctx, chartArea, PPT_GRADIENT_BLUE_START, PPT_GRADIENT_BLUE_END); } } ] @@ -1576,8 +1485,8 @@ const incomeBarData = computed(() => ({ backgroundColor: (context) => { const { chart } = context; const { ctx, chartArea } = chart; - if (!chartArea) return 'rgba(82,196,26,0.55)'; - return makeVerticalGradient(ctx, chartArea, 'rgba(82,196,26,0.80)', 'rgba(82,196,26,0.25)'); + if (!chartArea) return PPT_GREEN; + return makeVerticalGradient(ctx, chartArea, PPT_GREEN, '#73D13D'); } } ] @@ -1832,8 +1741,8 @@ const decisionFactorsBarData = computed(() => { backgroundColor: (context) => { const { chart } = context; const { ctx, chartArea } = chart; - if (!chartArea) return 'rgba(91,137,255,0.55)'; - return makeVerticalGradient(ctx, chartArea, 'rgba(91,137,255,0.85)', 'rgba(91,137,255,0.25)'); + if (!chartArea) return PPT_BLUE; + return makeVerticalGradient(ctx, chartArea, PPT_GRADIENT_BLUE_START, PPT_GRADIENT_BLUE_END); } } ] @@ -1866,8 +1775,8 @@ const refusalReasonsBarData = computed(() => { backgroundColor: (context) => { const { chart } = context; const { ctx, chartArea } = chart; - if (!chartArea) return 'rgba(255,142,79,0.55)'; - return makeVerticalGradient(ctx, chartArea, 'rgba(255,142,79,0.85)', 'rgba(255,142,79,0.25)'); + if (!chartArea) return PPT_ORANGE; + return makeVerticalGradient(ctx, chartArea, PPT_ORANGE, '#FFBB96'); } } ] diff --git a/src/views/pages/marketing/MarketingAnalysis.vue b/src/views/pages/marketing/MarketingAnalysis.vue index 4dc2a43..9846fbb 100644 --- a/src/views/pages/marketing/MarketingAnalysis.vue +++ b/src/views/pages/marketing/MarketingAnalysis.vue @@ -957,16 +957,16 @@ const drawBarValues = (chart) => { // Горизонтальный: цифра справа темная ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937'; ctx.textAlign = 'left'; - ctx.fillText(label, bar.x + 8, bar.y); + ctx.fillText(label, bar.x + 8, bar.y); } else { // === ВЕРТИКАЛЬНЫЙ (КАК НА ФОТО) === const barHeight = Math.abs(bar.base - bar.y); - + // Если столбец достаточно высокий (>25px), рисуем цифру ВНУТРИ белым цветом if (barHeight > 25) { - ctx.fillStyle = '#ffffff'; + ctx.fillStyle = '#ffffff'; // bar.y - это верхушка столбца. Добавляем 15px, чтобы спустить цифру чуть ниже верха - ctx.fillText(label, bar.x, bar.y + 15); + ctx.fillText(label, bar.x, bar.y + 15); } else { // Если столбец мелкий, рисуем цифру СВЕРХУ темным цветом ctx.fillStyle = document.documentElement.classList.contains('dark') ? '#ffffff' : '#1f2937'; @@ -991,7 +991,7 @@ const photoStyleBarOptions = { }, plugins: { legend: { display: false }, // Легенда не нужна - tooltip: { + tooltip: { enabled: true, callbacks: { label: (ctx) => `${ctx.formattedValue}%` } } @@ -1003,7 +1003,7 @@ const photoStyleBarOptions = { grid: { display: true, drawBorder: false, color: 'rgba(0,0,0,0.05)' }, ticks: { font: { size: 10 } } }, - x: { + x: { grid: { display: false }, // Вертикальные линии убираем ticks: { font: { size: 12, weight: '600' }, @@ -1025,8 +1025,8 @@ const horizontalBarChartOptions = { }, plugins: { legend: { display: false } }, scales: { - x: { display: false, beginAtZero: true }, - y: { + x: { display: false, beginAtZero: true }, + y: { grid: { display: false }, ticks: { font: { size: 12, weight: '500' }, crossAlign: 'far' } } @@ -1058,8 +1058,8 @@ const conversionFunnelChartOptions = { responsive: true, maintainAspectRatio: false, plugins: { legend: { display: true, position: 'bottom' } }, - scales: { - y: { beginAtZero: true, title: { display: true, text: 'Пользователи' } } + scales: { + y: { beginAtZero: true, title: { display: true, text: 'Пользователи' } } } }; @@ -1996,12 +1996,12 @@ const buildChannelsChartData = (acquisitionChannels) => { if (!shares || typeof shares !== 'object') return null; // Превращаем ключи "instagram_target" в "Instagram Target" - const labels = Object.keys(shares).map(key => + const labels = Object.keys(shares).map(key => key.split('_').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ') ); // Чистим значения от знака % для графика - const values = Object.values(shares).map(val => + const values = Object.values(shares).map(val => (typeof val === 'string') ? parseFloat(val.replace('%', '')) : val ); @@ -2149,16 +2149,16 @@ const mapChartsDataToSegments = (chartsData) => { segments.push({ type: 'chart', key: 'ageStructure', - title: 'АРЫСТАНБЕК', + title: 'Возрастная структура', chartType: 'bar', data: { labels, datasets: [{ data: values, - // !!! ЦВЕТ КАК НА ФОТО (СИНИЙ) !!! - backgroundColor: '#60a5fa', + // !!! ЦВЕТ КАК НА ФОТО (СИНИЙ) !!! + backgroundColor: '#60a5fa', borderRadius: 8, // Скругление - barPercentage: 0.65, + barPercentage: 0.65, unit: '%' }] }, @@ -2177,7 +2177,7 @@ const mapChartsDataToSegments = (chartsData) => { values = parsed.values; } else { labels = ['Низкий', 'Средний', 'Высокий']; - values = [20, 50, 30]; + values = [20, 50, 30]; } segments.push({ From e0e0ebfb1322030dc629239a06c0f3b645500077 Mon Sep 17 00:00:00 2001 From: arys Date: Thu, 22 Jan 2026 17:16:59 +0500 Subject: [PATCH 05/17] fix --- src/views/pages/MarketingAnalysis.vue | 237 ++++++++++++++++---------- 1 file changed, 151 insertions(+), 86 deletions(-) diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue index a2b5572..2b4cf47 100644 --- a/src/views/pages/MarketingAnalysis.vue +++ b/src/views/pages/MarketingAnalysis.vue @@ -160,7 +160,7 @@
{{ marketYoYGrowthValue }}
- +
@@ -465,7 +465,7 @@ >
- +
@@ -479,7 +479,7 @@ >
- +
@@ -528,37 +528,17 @@ /> -
- -
-
+
-
-
Основные проблемы на воронке
+
+
Воронка продаж
-
-
-
Этап
-
Проблема
-
-
-
-
Лид → Запись
-
{{ report?.sections?.customer_journey?.funnel_bottlenecks?.lead_to_booking }}
-
-
-
Запись → Оплата
-
{{ report?.sections?.customer_journey?.funnel_bottlenecks?.booking_to_payment }}
-
-
+
+
@@ -615,7 +595,7 @@ >
- +
@@ -1265,61 +1245,109 @@ const ageRows = computed(() => { }); }); +const barValuesPlugin = { + id: 'barValues', + afterDatasetsDraw(chart) { + const { ctx, data } = chart; + ctx.save(); + ctx.font = 'bold 15px Inter, system-ui, sans-serif'; + ctx.textBaseline = 'middle'; + + data.datasets.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i); + if (!meta.hidden) { + meta.data.forEach((bar, index) => { + const value = dataset.data[index]; + if (value !== null && value !== undefined && value !== 0) { + const unit = dataset.unit || ''; + let label = value.toLocaleString(); + if (typeof value === 'number' && !Number.isInteger(value) && value < 10) { + label = value.toFixed(2); + } + label += unit; + const isHorizontal = chart.options.indexAxis === 'y'; + + if (isHorizontal) { + const barWidth = Math.abs(bar.x - bar.base); + if (barWidth > 60) { + ctx.fillStyle = '#ffffff'; + ctx.textAlign = 'right'; + ctx.fillText(label, bar.x - 12, bar.y); + } else { + ctx.fillStyle = '#64748b'; + ctx.textAlign = 'left'; + ctx.fillText(label, bar.x + 10, bar.y); + } + } else { + const barHeight = Math.abs(bar.base - bar.y); + ctx.textAlign = 'center'; + if (barHeight > 35) { + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 18px Inter, system-ui, sans-serif'; + ctx.fillText(label, bar.x, bar.y + 20); + } else { + ctx.fillStyle = '#64748b'; + ctx.font = 'bold 14px Inter, system-ui, sans-serif'; + ctx.fillText(label, bar.x, bar.y - 15); + } + } + } + }); + } + }); + ctx.restore(); + } +}; + +const donutPercentPlugin = { + id: 'donutPercent', + afterDatasetsDraw(chart) { + const ctx = chart.ctx; + const data = chart.data.datasets[0]?.data || []; + const total = data.reduce((a, b) => a + b, 0); + + if (total === 0) return; + + chart.data.datasets.forEach((dataset, i) => { + const meta = chart.getDatasetMeta(i); + meta.data.forEach((element, index) => { + const value = dataset.data[index]; + const percentage = total > 0 ? ((value / total) * 100).toFixed(1) : 0; + + if (parseFloat(percentage) < 3) return; + + const arc = element; + const { x, y, startAngle, endAngle, innerRadius, outerRadius } = arc; + + const angle = (startAngle + endAngle) / 2; + const radius = (innerRadius + outerRadius) / 2; + + const centerX = x + Math.cos(angle) * radius; + const centerY = y + Math.sin(angle) * radius; + + ctx.save(); + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold 14px Inter, system-ui, sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.shadowColor = 'rgba(0, 0, 0, 0.4)'; + ctx.shadowBlur = 4; + ctx.shadowOffsetX = 1; + ctx.shadowOffsetY = 1; + ctx.fillText(`${percentage}%`, centerX, centerY); + ctx.restore(); + }); + }); + } +}; + +const barPlugins = [barValuesPlugin]; +const donutPlugins = [donutPercentPlugin]; + const compactBarOptions = computed(() => ({ maintainAspectRatio: false, plugins: { - legend: { display: false }, - barValues: { - id: 'barValues', - afterDatasetsDraw(chart) { - const { ctx, data } = chart; - ctx.save(); - ctx.font = 'bold 12px Inter, system-ui, sans-serif'; - ctx.textBaseline = 'middle'; - - data.datasets.forEach((dataset, i) => { - const meta = chart.getDatasetMeta(i); - if (!meta.hidden) { - meta.data.forEach((bar, index) => { - const value = dataset.data[index]; - if (value !== null && value !== undefined && value !== 0) { - const unit = dataset.unit || ''; - const label = value.toLocaleString() + unit; - const isHorizontal = chart.options.indexAxis === 'y'; - - if (isHorizontal) { - const barWidth = Math.abs(bar.x - bar.base); - if (barWidth > 40) { - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 12px Inter, system-ui, sans-serif'; - ctx.textAlign = 'right'; - ctx.fillText(label, bar.x - 12, bar.y); - } else { - ctx.fillStyle = '#64748b'; - ctx.font = 'bold 11px Inter, system-ui, sans-serif'; - ctx.textAlign = 'left'; - ctx.fillText(label, bar.x + 10, bar.y); - } - } else { - const barHeight = Math.abs(bar.base - bar.y); - ctx.textAlign = 'center'; - if (barHeight > 25) { - ctx.fillStyle = '#ffffff'; - ctx.font = 'bold 14px Inter, system-ui, sans-serif'; - ctx.fillText(label, bar.x, bar.y + 16); - } else { - ctx.fillStyle = '#64748b'; - ctx.font = 'bold 12px Inter, system-ui, sans-serif'; - ctx.fillText(label, bar.x, bar.y - 12); - } - } - } - }); - } - }); - ctx.restore(); - } - } + legend: { display: false } }, scales: { x: { ticks: { color: '#64748B', font: { size: 11 } }, grid: { display: false, drawBorder: false } }, @@ -1831,6 +1859,43 @@ const channelSharesTableRows = computed(() => { ]; }); +const funnelBarData = computed(() => { + const fm = report.value?.sections?.customer_journey?.funnel_metrics ?? {}; + const labels = ['Охват/Трафик', 'Визиты на сайт', 'Лиды', 'Записи/Бронь', 'Оплаты']; + const values = [fm.ad_traffic, fm.site_visitors, fm.leads, fm.bookings, fm.payments].map((v) => { + if (typeof v === 'string') return Number(v.replace(/[^\d.]/g, '')); + return Number(v) || 0; + }); + + return { + labels, + datasets: [ + { + data: values, + borderRadius: 10, + backgroundColor: (context) => { + const { chart } = context; + const { ctx, chartArea } = chart; + if (!chartArea) return PPT_BLUE; + return makeVerticalGradient(ctx, chartArea, PPT_GRADIENT_BLUE_START, PPT_GRADIENT_BLUE_END); + } + } + ] + }; +}); + +const funnelBarOptions = computed(() => { + const base = compactBarOptions.value; + return { + ...base, + indexAxis: 'y', + scales: { + x: { ...base.scales.x, grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false } }, + y: { ...base.scales.y, grid: { display: false, drawBorder: false } } + } + }; +}); + const channelSharesBarOptions = computed(() => { const base = compactBarOptions.value; return { From 6e37ab79161f5e5a83a2859b7fa54be10ab6986e Mon Sep 17 00:00:00 2001 From: root Date: Thu, 22 Jan 2026 20:16:45 +0500 Subject: [PATCH 06/17] . --- src/views/pages/MarketingAnalysis.vue | 37 ++++++++++++++++++- .../pages/marketing/MarketingAnalysis.vue | 14 +++---- 2 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue index 2b4cf47..52b8578 100644 --- a/src/views/pages/MarketingAnalysis.vue +++ b/src/views/pages/MarketingAnalysis.vue @@ -917,12 +917,28 @@ + + +
+
+
+
+
+