This commit is contained in:
root
2026-01-17 15:08:36 +05:00
parent f7626d77b7
commit fb8586d450
3 changed files with 287 additions and 21 deletions
+191 -17
View File
@@ -162,6 +162,20 @@
<div class="h-[260px]">
<Chart type="line" :data="marketDynamicsLineData" :options="marketDynamicsLineOptions" />
</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 marketDynamicsTableRows" :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>
@@ -296,6 +310,20 @@
<div class="h-[240px]">
<Chart type="bar" :data="ageBarData" :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 ageRows" :key="row.key" 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">
@@ -310,6 +338,20 @@
<div class="h-[220px]">
<Chart type="bar" :data="incomeBarData" :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 incomeTableRows" :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>
@@ -372,6 +414,20 @@
<div class="h-[280px]">
<Chart type="bar" :data="visibilityBarData" :options="visibilityBarOptions" />
</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 comparisonRows" :key="row.key" 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.visibility ?? '—' }}</div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="col-span-12 lg:col-span-5">
@@ -467,6 +523,20 @@
</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">
@@ -483,6 +553,20 @@
<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">
@@ -497,6 +581,20 @@
<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>
@@ -633,6 +731,20 @@
<div class="h-[280px]">
<Chart type="bar" :data="channelSharesBarData" :options="channelSharesBarOptions" />
</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 channelSharesTableRows" :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-5">
@@ -1171,25 +1283,25 @@ function hexToRgba(hex, alpha) {
}
// Charts: Market dynamics (line / area)
const MARKET_MONTHS = [
{ key: 'january', label: 'Янв' },
{ key: 'february', label: 'Фев' },
{ key: 'march', label: 'Мар' },
{ key: 'april', label: 'Апр' },
{ key: 'may', label: 'Май' },
{ key: 'june', label: 'Июн' },
{ key: 'july', label: 'Июл' },
{ key: 'august', label: 'Авг' },
{ key: 'september', label: 'Сен' },
{ key: 'october', label: 'Окт' },
{ key: 'november', label: 'Ноя' },
{ key: 'december', label: 'Дек' }
];
const marketDynamicsLineData = computed(() => {
const monthly = report.value?.sections?.market_overview?.monthly_online_index ?? {};
const months = [
{ key: 'january', label: 'Янв' },
{ key: 'february', label: 'Фев' },
{ key: 'march', label: 'Мар' },
{ key: 'april', label: 'Апр' },
{ key: 'may', label: 'Май' },
{ key: 'june', label: 'Июн' },
{ key: 'july', label: 'Июл' },
{ key: 'august', label: 'Авг' },
{ key: 'september', label: 'Сен' },
{ key: 'october', label: 'Окт' },
{ key: 'november', label: 'Ноя' },
{ key: 'december', label: 'Дек' }
];
const labels = months.map((m) => m.label);
const values = months.map((m) => {
const labels = MARKET_MONTHS.map((m) => m.label);
const values = MARKET_MONTHS.map((m) => {
const v = monthly?.[m.key];
return typeof v === 'number' ? v : 0;
});
@@ -1219,6 +1331,18 @@ const marketDynamicsLineData = computed(() => {
};
});
const marketDynamicsTableRows = computed(() => {
const monthly = report.value?.sections?.market_overview?.monthly_online_index ?? {};
return MARKET_MONTHS.map((m) => {
const raw = monthly?.[m.key];
const parsed = typeof raw === 'number' ? raw : Number(String(raw ?? '').replace(/[^\d.-]/g, ''));
return {
label: m.label,
value: Number.isFinite(parsed) ? parsed : '—'
};
});
});
const marketDynamicsLineOptions = computed(() => ({
maintainAspectRatio: false,
plugins: { legend: { display: false } },
@@ -1413,6 +1537,15 @@ const incomeBarData = computed(() => {
};
});
const incomeTableRows = computed(() => {
const above = aboveAvgIncomeValue.value;
const other = Math.max(0, 100 - above);
return [
{ label: 'Остальные', value: other },
{ label: 'Выше среднего', value: above }
];
});
// Segments list
const segmentRows = computed(() => {
const segs = report.value?.sections?.target_audience?.segments ?? [];
@@ -1628,6 +1761,16 @@ const queryTypesMetrics = computed(() => {
.filter((metric) => metric.value > 0);
});
const queryTypesTableRows = computed(() => {
const q = report.value?.sections?.demand_and_behavior?.search_query_types ?? {};
return [
{ label: 'Прямой интерес', value: percentToNumber(q.direct_interest_catalog) },
{ label: 'Поиск по вопросу', value: percentToNumber(q.question_based_articles) },
{ label: 'Конкуренты', value: percentToNumber(q.competitors_reviews) },
{ label: 'Решение проблемы', value: percentToNumber(q.problem_solving_hot_promo) }
];
});
const decisionFactorsBarData = computed(() => {
const f = report.value?.sections?.demand_and_behavior?.decision_factors ?? {};
const labels = ['Акция/скидка', 'Отзыв/рекоменд.', 'Гарантия/качество', 'Удобство/сервис', 'Наличие мест', 'Цена'];
@@ -1649,6 +1792,18 @@ const decisionFactorsBarData = computed(() => {
};
});
const decisionFactorsTableRows = computed(() => {
const f = report.value?.sections?.demand_and_behavior?.decision_factors ?? {};
return [
{ label: 'Акция/скидка', value: percentToNumber(f.promo_discount) },
{ label: 'Отзыв/рекоменд.', value: percentToNumber(f.review_recommendation) },
{ label: 'Гарантия/качество', value: percentToNumber(f.warranty_quality) },
{ label: 'Удобство/сервис', value: percentToNumber(f.convenience_service) },
{ label: 'Наличие мест', value: percentToNumber(f.availability) },
{ label: 'Цена', value: percentToNumber(f.price) }
];
});
const refusalReasonsBarData = computed(() => {
const r = report.value?.sections?.demand_and_behavior?.reasons_for_refusal ?? {};
const labels = ['Высокая цена', 'Нет отзывов', 'Сложности с выбором'];
@@ -1670,6 +1825,15 @@ const refusalReasonsBarData = computed(() => {
};
});
const refusalReasonsTableRows = computed(() => {
const r = report.value?.sections?.demand_and_behavior?.reasons_for_refusal ?? {};
return [
{ label: 'Высокая цена', value: percentToNumber(r.high_price) },
{ label: 'Нет отзывов', value: percentToNumber(r.no_reviews_social_proof) },
{ label: 'Сложности с выбором', value: percentToNumber(r.choice_difficulty_unclear_offer) }
];
});
// Acquisition channels charts/tables
const channelSharesBarData = computed(() => {
const shares = report.value?.sections?.acquisition_channels?.channel_shares ?? {};
@@ -1698,6 +1862,16 @@ const channelSharesBarData = computed(() => {
};
});
const channelSharesTableRows = computed(() => {
const shares = report.value?.sections?.acquisition_channels?.channel_shares ?? {};
return [
{ label: 'Яндекс Директ', value: percentToNumber(shares.yandex_direct) },
{ label: 'SEO (сиб.тайт)', value: percentToNumber(shares.seo) },
{ label: 'Ретаргетинг', value: percentToNumber(shares.retargeting) },
{ label: 'VKontakte', value: percentToNumber(shares.vkontakte) }
];
});
const channelSharesBarOptions = computed(() => ({
indexAxis: 'y',
maintainAspectRatio: false,