fix
This commit is contained in:
@@ -266,39 +266,9 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 lg:col-span-5">
|
||||
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4 h-full">
|
||||
<div class="flex items-center gap-1.5 mb-3">
|
||||
<div class="text-[16px] font-semibold text-slate-800">Сегменты аудитории для рекламы</div>
|
||||
<i
|
||||
class="pi pi-question-circle text-[12px] text-slate-400 cursor-help"
|
||||
v-tooltip.top="'Массив ключевых групп потребителей с указанием их названия и процентной доли в общей массе. Помогает определить приоритетные сегменты для таргетинга.'"
|
||||
></i>
|
||||
</div>
|
||||
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
||||
<div class="grid grid-cols-12 bg-slate-50 text-[12px] font-semibold text-slate-600">
|
||||
<div class="col-span-8 px-3 py-2">Сегмент</div>
|
||||
<div class="col-span-4 px-3 py-2 text-right">Доля</div>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200/70">
|
||||
<div v-for="seg in segmentRows" :key="seg.name" class="px-3 py-3">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<div class="text-[14px] text-slate-800 truncate">{{ seg.name }}</div>
|
||||
<div class="text-[14px] font-semibold text-slate-800">{{ seg.value }}%</div>
|
||||
</div>
|
||||
<div class="mt-2 h-3 rounded-full bg-slate-200/70 overflow-hidden">
|
||||
<div class="h-full rounded-full" :style="{ width: `${seg.value}%`, background: `linear-gradient(90deg, ${PPT_BLUE}, rgba(91,137,255,0.25))` }"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 lg:col-span-7">
|
||||
<div class="col-span-12">
|
||||
<div class="grid grid-cols-12 gap-4">
|
||||
<div class="col-span-12">
|
||||
<div class="col-span-12 lg:col-span-6">
|
||||
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4">
|
||||
<div class="flex items-center gap-1.5 mb-2">
|
||||
<div class="text-[16px] font-semibold text-slate-800">Возрастная структура</div>
|
||||
@@ -312,7 +282,7 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12">
|
||||
<div class="col-span-12 lg:col-span-6">
|
||||
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4">
|
||||
<div class="flex items-center gap-1.5 mb-2">
|
||||
<div class="text-[16px] font-semibold text-slate-800">Доход и платёжеспособность</div>
|
||||
@@ -481,20 +451,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-slate-200/70">
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
||||
<div class="grid grid-cols-12 bg-slate-50 text-[12px] font-semibold text-slate-600">
|
||||
<div class="col-span-8 px-3 py-2">Тип запроса</div>
|
||||
<div class="col-span-4 px-3 py-2 text-right">Доля</div>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200/70">
|
||||
<div v-for="row in queryTypesTableRows" :key="row.label" class="grid grid-cols-12 px-3 py-2.5">
|
||||
<div class="col-span-8 text-[13px] text-slate-800">{{ row.label }}</div>
|
||||
<div class="col-span-4 text-[13px] text-slate-800 text-right">{{ row.value }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 lg:col-span-7">
|
||||
@@ -511,20 +467,6 @@
|
||||
<div class="h-[220px]">
|
||||
<Chart type="bar" :data="decisionFactorsBarData" :options="compactBarOptions" />
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-slate-200/70">
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
||||
<div class="grid grid-cols-12 bg-slate-50 text-[12px] font-semibold text-slate-600">
|
||||
<div class="col-span-8 px-3 py-2">Фактор</div>
|
||||
<div class="col-span-4 px-3 py-2 text-right">Доля</div>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200/70">
|
||||
<div v-for="row in decisionFactorsTableRows" :key="row.label" class="grid grid-cols-12 px-3 py-2.5">
|
||||
<div class="col-span-8 text-[13px] text-slate-800">{{ row.label }}</div>
|
||||
<div class="col-span-4 text-[13px] text-slate-800 text-right">{{ row.value }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12">
|
||||
@@ -539,20 +481,6 @@
|
||||
<div class="h-[200px]">
|
||||
<Chart type="bar" :data="refusalReasonsBarData" :options="compactBarOptions" />
|
||||
</div>
|
||||
<div class="mt-4 pt-4 border-t border-slate-200/70">
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
||||
<div class="grid grid-cols-12 bg-slate-50 text-[12px] font-semibold text-slate-600">
|
||||
<div class="col-span-8 px-3 py-2">Причина</div>
|
||||
<div class="col-span-4 px-3 py-2 text-right">Доля</div>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200/70">
|
||||
<div v-for="row in refusalReasonsTableRows" :key="row.label" class="grid grid-cols-12 px-3 py-2.5">
|
||||
<div class="col-span-8 text-[13px] text-slate-800">{{ row.label }}</div>
|
||||
<div class="col-span-4 text-[13px] text-slate-800 text-right">{{ row.value }}%</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -677,7 +605,7 @@
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12 lg:col-span-7">
|
||||
<div class="col-span-12">
|
||||
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4 h-full">
|
||||
<div class="flex items-center gap-1.5 mb-2">
|
||||
<div class="text-[16px] font-semibold text-slate-800">Доли каналов</div>
|
||||
@@ -691,31 +619,6 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-12 lg:col-span-5">
|
||||
<div class="rounded-2xl bg-white shadow-sm border border-slate-200/60 p-4 h-full">
|
||||
<div class="flex items-center gap-1.5 mb-3">
|
||||
<div class="text-[16px] font-semibold text-slate-800">Стоимость привлечения</div>
|
||||
<i
|
||||
class="pi pi-question-circle text-[12px] text-slate-400 cursor-help"
|
||||
v-tooltip.top="'Детальная оценка каждого канала: стоимость привлечения (cost) и качественный статус (status: Эффективный, Средний, Низкий). Помогает оптимизировать распределение бюджета.'"
|
||||
></i>
|
||||
</div>
|
||||
<div class="overflow-hidden rounded-xl border border-slate-200/70">
|
||||
<div class="grid grid-cols-12 bg-slate-50 text-[12px] font-semibold text-slate-600">
|
||||
<div class="col-span-5 px-3 py-2">Канал</div>
|
||||
<div class="col-span-3 px-3 py-2 text-center">Стоимость</div>
|
||||
<div class="col-span-4 px-3 py-2">Эффективность</div>
|
||||
</div>
|
||||
<div class="divide-y divide-slate-200/70">
|
||||
<div v-for="row in channelEfficiencyRows" :key="row.key" class="grid grid-cols-12 px-3 py-3 items-center">
|
||||
<div class="col-span-5 text-[13px] text-slate-800">{{ row.label }}</div>
|
||||
<div class="col-span-3 text-[13px] text-slate-800 text-center">{{ row.cost }}</div>
|
||||
<div class="col-span-4 text-[13px]" :class="row.statusClass">{{ row.status }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-span-12">
|
||||
<PptConclusionBox :text="report?.sections?.acquisition_channels?.conclusion" />
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user