.
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user