From cfd306866bd3928133355cab0da0c01d396cb020 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 22 Jan 2026 21:41:16 +0500 Subject: [PATCH] . --- src/views/pages/MarketingAnalysis.vue | 181 ++++++++++++++++-- .../pages/marketing/MarketingAnalysis.vue | 12 ++ 2 files changed, 173 insertions(+), 20 deletions(-) diff --git a/src/views/pages/MarketingAnalysis.vue b/src/views/pages/MarketingAnalysis.vue index 52b8578..4a498b8 100644 --- a/src/views/pages/MarketingAnalysis.vue +++ b/src/views/pages/MarketingAnalysis.vue @@ -588,14 +588,17 @@
-
Доли каналов
+
Эффективность каналов
- + +
+ Нет данных об эффективности каналов +
@@ -1943,38 +1946,176 @@ const channelSharesBarOptions = computed(() => { }; }); +// Функция для извлечения числового значения из стоимости (например, "2500 KZT" -> 2500) +function extractCostNumber(costStr) { + if (!costStr || costStr === '—') return 0; + const num = Number(String(costStr).replace(/[^\d.]/g, '')); + return Number.isFinite(num) ? num : 0; +} + +// График эффективности каналов (стоимость привлечения) +const channelEfficiencyBarData = computed(() => { + const eff = report.value?.sections?.acquisition_channels?.channel_efficiency ?? {}; + + // Маппинг ключей каналов на читаемые названия + const channelLabels = { + 'yandex': 'Яндекс', + 'yandex_direct': 'Яндекс Директ', + 'seo': 'SEO', + 'retargeting': 'Ретаргет', + 'vkontakte': 'VKontakte', + 'youtube': 'YouTube', + 'instagram': 'Instagram', + 'tiktok': 'TikTok', + 'kaspi': 'Kaspi', + 'google_search': 'Google Поиск', + '2gis': '2GIS', + 'google_ads': 'Google Ads', + 'facebook': 'Facebook' + }; + + function getChannelLabel(key) { + return channelLabels[key] || key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + } + + // Цвета для разных каналов + const channelColors = [ + PPT_BLUE, + PPT_ORANGE, + PPT_GREEN, + '#60A5FA', + '#A855F7', + '#EC4899', + '#F59E0B', + '#10B981', + '#3B82F6', + '#8B5CF6' + ]; + + // Получаем все каналы из данных + const items = Object.keys(eff) + .map((key, index) => { + const row = eff[key]; + if (!row || !row.cost) return null; + return { + key: key, + label: getChannelLabel(key), + cost: row.cost, + costValue: extractCostNumber(row.cost), + color: channelColors[index % channelColors.length] + }; + }) + .filter(Boolean) + .sort((a, b) => a.costValue - b.costValue); // Сортируем по стоимости (от меньшей к большей) + + return { + labels: items.map((i) => i.label), + datasets: [ + { + data: items.map((i) => i.costValue), + borderRadius: 12, + borderSkipped: false, + backgroundColor: (context) => { + const { chart, dataIndex } = context; + const { ctx, chartArea } = chart; + const base = items[dataIndex]?.color ?? PPT_BLUE; + if (!chartArea) return hexToRgba(base, 0.55); + return makeVerticalGradient(ctx, chartArea, hexToRgba(base, 0.88), hexToRgba(base, 0.24)); + } + } + ] + }; +}); + +const channelEfficiencyBarOptions = computed(() => { + const base = compactBarOptions.value; + return { + ...base, + indexAxis: 'y', + scales: { + x: { + ...base.scales.x, + grid: { color: 'rgba(148,163,184,0.15)', drawBorder: false }, + ticks: { + callback: function(value) { + return value.toLocaleString('ru-RU') + ' KZT'; + } + } + }, + y: { ...base.scales.y, grid: { display: false, drawBorder: false } } + }, + plugins: { + ...base.plugins, + tooltip: { + ...base.plugins?.tooltip, + callbacks: { + label: function(context) { + return context.parsed.x.toLocaleString('ru-RU') + ' KZT'; + } + } + } + } + }; +}); + const channelEfficiencyRows = computed(() => { const eff = report.value?.sections?.acquisition_channels?.channel_efficiency ?? {}; - const order = [ - { key: 'yandex', label: 'Яндекс' }, - { key: 'seo', label: 'SEO (сиб.тайт)' }, - { key: 'retargeting', label: 'Ретаргет' }, - { key: 'vkontakte', label: 'VKontakte' }, - { key: 'youtube', label: 'YouTube' }, - { key: 'instagram', label: 'Instagram' } - ]; + + // Маппинг ключей каналов на читаемые названия + const channelLabels = { + 'yandex': 'Яндекс', + 'yandex_direct': 'Яндекс Директ', + 'seo': 'SEO', + 'retargeting': 'Ретаргет', + 'vkontakte': 'VKontakte', + 'youtube': 'YouTube', + 'instagram': 'Instagram', + 'tiktok': 'TikTok', + 'kaspi': 'Kaspi', + 'google_search': 'Google Поиск', + '2gis': '2GIS', + 'google_ads': 'Google Ads', + 'facebook': 'Facebook' + }; + + function getChannelLabel(key) { + return channelLabels[key] || key.split('_').map(word => word.charAt(0).toUpperCase() + word.slice(1)).join(' '); + } function statusClass(status) { const s = String(status || '').toLowerCase(); - if (s.includes('эффектив')) return 'text-green-700'; - if (s.includes('удовлетвор')) return 'text-amber-700'; - if (s.includes('завыш')) return 'text-red-700'; + // Обработка английских статусов + if (s.includes('very high') || s.includes('очень высок') || s.includes('высок')) return 'text-green-700 font-semibold'; + if (s.includes('high') || s.includes('эффектив')) return 'text-green-700'; + if (s.includes('medium') || s.includes('средн') || s.includes('удовлетвор')) return 'text-amber-700'; + if (s.includes('low') || s.includes('низк') || s.includes('завыш')) return 'text-red-700'; + if (s.includes('local') || s.includes('локальн')) return 'text-blue-700'; return 'text-slate-600'; } - return order - .map((o) => { - const row = eff[o.key]; + // Получаем все каналы из данных и преобразуем в массив + return Object.keys(eff) + .map((key) => { + const row = eff[key]; if (!row) return null; return { - key: o.key, - label: o.label, + key: key, + label: getChannelLabel(key), cost: row.cost ?? '—', status: row.status ?? '—', statusClass: statusClass(row.status) }; }) - .filter(Boolean); + .filter(Boolean) + .sort((a, b) => { + // Сортируем по статусу: Very High -> High -> Medium -> Local -> остальные + const statusOrder = { 'very high': 1, 'high': 2, 'medium': 3, 'local': 4 }; + const aStatus = String(a.status || '').toLowerCase(); + const bStatus = String(b.status || '').toLowerCase(); + const aOrder = statusOrder[aStatus] || 5; + const bOrder = statusOrder[bStatus] || 5; + return aOrder - bOrder; + }); }); // Economics tables diff --git a/src/views/pages/marketing/MarketingAnalysis.vue b/src/views/pages/marketing/MarketingAnalysis.vue index 05d8f26..a7b88b4 100644 --- a/src/views/pages/marketing/MarketingAnalysis.vue +++ b/src/views/pages/marketing/MarketingAnalysis.vue @@ -110,6 +110,13 @@ {{ errors.weakSide }} +
+ + + {{ errors.marketingBudget }} + Укажите ваш бюджет на маркетинг (опционально) +
+
@@ -800,6 +807,7 @@ import markedKatex from 'marked-katex-extension'; import Button from 'primevue/button'; import Chart from 'primevue/chart'; import Checkbox from 'primevue/checkbox'; +import InputNumber from 'primevue/inputnumber'; import InputText from 'primevue/inputtext'; import MultiSelect from 'primevue/multiselect'; import ProgressSpinner from 'primevue/progressspinner'; @@ -862,6 +870,7 @@ const formData = ref({ detailLevel: 'СТАНДАРТНО', strongSide: '', weakSide: '', + marketingBudget: null, analysisType: [] }); @@ -1313,6 +1322,9 @@ const handleSubmit = async () => { if (formData.value.weakSide && formData.value.weakSide.trim()) { requestData.weakSide = formData.value.weakSide.trim(); } + if (formData.value.marketingBudget !== null && formData.value.marketingBudget !== undefined) { + requestData.marketing_budget = formData.value.marketingBudget; + } const result = await MarketingService.startAnalysis(requestData, { generateV2: true });