This commit is contained in:
arys
2026-04-07 16:01:23 +05:00
parent 02ebaf87f6
commit 5432d679fa
23 changed files with 4750 additions and 1281 deletions
+65 -42
View File
@@ -1,33 +1,48 @@
/* =========================================================
/* =========================================================
KAI v4.0 Dashboard — Dark Theme Globals
Loaded by MarketingAnalysisV3.vue via <style>
========================================================= */
@import url('https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;900&family=Onest:wght@400;500;600;700&display=swap');
:root {
--kai-bg: #0A0E1A;
--kai-bg2: #0D1428;
--kai-card: rgba(255,255,255,0.04);
--kai-card-hover: rgba(255,255,255,0.07);
--kai-border: rgba(45,123,255,0.15);
--kai-border-hover: rgba(45,123,255,0.4);
--kai-bg: #07090F;
--kai-bg2: #0A0D14;
--kai-card: rgba(255,255,255,0.03);
--kai-card-hover: rgba(255,255,255,0.06);
--kai-border: rgba(45,123,255,0.12);
--kai-border-hover: rgba(45,123,255,0.3);
--kai-blue: #2D7BFF;
--kai-green: #00D48B;
--kai-orange: #FF7A2D;
--kai-purple: #A855F7;
--kai-blue-rgb: 45, 123, 255;
--kai-green: #10B981;
--kai-green-rgb: 16, 185, 129;
--kai-orange: #F97316;
--kai-purple: #8B5CF6;
--kai-yellow: #F59E0B;
--kai-red: #EF4444;
--kai-txt: rgba(255,255,255,0.92);
--kai-txt2: rgba(255,255,255,0.60);
--kai-txt3: rgba(255,255,255,0.35);
--kai-txt: rgba(255,255,255,0.95);
--kai-txt2: rgba(255,255,255,0.65);
--kai-txt3: rgba(255,255,255,0.40);
/* Font size unification - smaller & professional */
--kai-fs-base: 12.5px;
--kai-fs-sm: 11px;
--kai-fs-xs: 9.5px;
--kai-fs-lg: 14px;
--kai-fs-xl: 18px;
--kai-fs-title: 22px;
}
/* Layout */
.kai-page {
background: var(--kai-bg);
background-image:
radial-gradient(at 0% 0%, rgba(var(--kai-blue-rgb), 0.05) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(var(--kai-green-rgb), 0.03) 0px, transparent 50%);
min-height: 100vh;
font-family: 'Onest', sans-serif;
color: var(--kai-txt);
font-size: var(--kai-fs-base);
-webkit-font-smoothing: antialiased;
}
/* Header */
@@ -46,18 +61,19 @@
}
.kai-logo {
width: 40px;
height: 40px;
background: var(--kai-blue);
border-radius: 8px;
width: 34px;
height: 34px;
background: linear-gradient(135deg, var(--kai-blue), #1e60ff);
border-radius: 9px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Unbounded', sans-serif;
font-weight: 900;
font-size: 13px;
font-size: 11px;
color: #fff;
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(var(--kai-blue-rgb), 0.3);
}
.kai-title-area {
@@ -68,27 +84,31 @@
.kai-title-niche {
font-family: 'Unbounded', sans-serif;
font-weight: 700;
font-size: 13px;
font-size: 11.5px;
color: var(--kai-txt);
line-height: 1;
letter-spacing: -0.01em;
}
.kai-title-sub {
font-size: 11px;
color: var(--kai-txt2);
margin-top: 2px;
font-size: 9.5px;
font-weight: 600;
color: var(--kai-txt3);
margin-top: 3px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.kai-badge-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
gap: 5px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
font-size: 9px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.05em;
letter-spacing: 0.08em;
}
.kai-badge-status.green {
background: rgba(0,212,139,0.12);
@@ -138,31 +158,33 @@
border-color: rgba(45,123,255,0.25);
}
.kai-section-header {
padding: 20px 28px 18px;
padding: 16px 24px 14px;
border-bottom: 1px solid var(--kai-border);
background: rgba(45,123,255,0.03);
background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.05), transparent);
}
.kai-section-title {
font-family: 'Unbounded', sans-serif;
font-weight: 700;
font-size: 17px;
font-size: 14.5px;
color: var(--kai-txt);
line-height: 1.2;
margin: 0;
letter-spacing: -0.01em;
}
.kai-section-sub {
font-size: 12px;
font-size: 10.5px;
color: var(--kai-txt2);
margin-top: 4px;
margin-top: 3px;
font-weight: 500;
}
.kai-section-badge {
font-family: 'Unbounded', sans-serif;
font-size: 11px;
font-size: 9px;
font-weight: 900;
color: var(--kai-blue);
opacity: 0.5;
margin-top: 2px;
opacity: 0.4;
margin-top: 1px;
letter-spacing: 0.1em;
}
.kai-section-body {
padding: 24px 28px;
@@ -210,22 +232,23 @@
}
.kai-kpi-num {
font-family: 'Unbounded', sans-serif;
font-size: 36px;
font-size: 28px;
font-weight: 900;
line-height: 1;
margin-bottom: 6px;
margin-bottom: 4px;
color: var(--kai-txt);
letter-spacing: -0.03em;
}
.kai-kpi-label {
font-size: 11px;
font-weight: 600;
font-size: 9.5px;
font-weight: 700;
color: var(--kai-txt2);
text-transform: uppercase;
letter-spacing: 0.06em;
margin-bottom: 8px;
letter-spacing: 0.08em;
margin-bottom: 6px;
}
.kai-kpi-sub {
font-size: 12px;
font-size: 10.5px;
color: var(--kai-txt3);
line-height: 1.4;
}
@@ -399,68 +399,61 @@ export default {
</script>
<template>
<div class="marketing-brief-wizard-v3 bg-surface-50 dark:bg-surface-900 min-h-screen p-6">
<div class="marketing-brief-wizard-v3 kai-page p-4 md:p-8">
<Toast />
<div class="max-w-4xl mx-auto">
<div class="card">
<div class="card-header mb-6">
<div class="flex items-center justify-between gap-4 mb-4">
<div class="max-w-3xl mx-auto">
<!-- Glass Card Container -->
<div class="kai-section min-h-[600px] flex flex-col">
<!-- 1. HEADER SECTION -->
<div class="kai-section-header flex flex-col gap-4">
<div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-3">
<div class="kai-logo">KAI</div>
<div>
<div class="flex items-center gap-2 mb-1">
<Button icon="pi pi-arrow-left" text rounded size="small" class="text-surface-500 p-0 w-6 h-6 mr-1" @click="$router.push('/marketing-analysis')" v-tooltip.top="'На главную Маркетинга'" />
<h1 class="text-2xl font-bold text-surface-900 dark:text-surface-0 m-0">Бриф для маркетингового анализа v4.0</h1>
<h1 class="kai-section-title">Создание ИИ-Брифа</h1>
<p class="kai-section-sub">KAI Prime · Intelligent Onboarding</p>
</div>
<p class="text-sm text-surface-500 dark:text-surface-400 mt-1">{{ STEPS[currentStep] }} · шаг {{ currentStep + 1 }} из {{ STEPS.length }}</p>
</div>
<div class="hidden sm:block text-right text-xs text-surface-500 dark:text-surface-400">
<p>Можно пройти за 57 минут.</p>
<p>Данные нужны, чтобы ИИ дал максимально точные выводы.</p>
<Button icon="pi pi-times" text rounded size="small" class="text-surface-500" @click="$router.push('/marketing-analysis')" />
</div>
<!-- Compact Stepper -->
<div class="flex items-center gap-2 mt-2">
<div v-for="(label, index) in STEPS" :key="index"
class="h-1 rounded-full flex-1 transition-all duration-500"
:class="[
index <= currentStep ? 'bg-blue-500 shadow-[0_0_8px_rgba(45,123,255,0.4)]' : 'bg-surface-800'
]">
</div>
</div>
<div class="relative h-2 overflow-hidden rounded-full bg-surface-200 dark:bg-surface-800 mb-4">
<div class="absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-primary-500 via-purple-500 to-pink-500 transition-all duration-300" :style="{ width: `${((currentStep + 1) / STEPS.length) * 100}%` }" />
</div>
<div class="flex flex-wrap gap-2 text-xs text-surface-500 dark:text-surface-400">
<div
v-for="(label, index) in STEPS"
:key="label"
class="flex items-center gap-1 rounded-full px-2 py-1"
:class="{
'bg-primary-50 text-primary-700 dark:bg-primary-900/40 dark:text-primary-200': index === currentStep,
'bg-emerald-50 text-emerald-700 dark:bg-emerald-900/40 dark:text-emerald-200': index < currentStep,
'bg-surface-100 dark:bg-surface-800': index > currentStep
}"
>
<i v-if="index < currentStep" class="pi pi-check-circle text-xs" />
<span v-else class="h-3 w-3 rounded-full bg-surface-300 dark:bg-surface-600" />
<span class="hidden sm:inline">{{ label }}</span>
<span class="sm:hidden">{{ index + 1 }}</span>
</div>
<div class="flex justify-between items-center text-[9px] font-black uppercase tracking-widest text-surface-500">
<span>Шаг {{ currentStep + 1 }} из {{ STEPS.length }}</span>
<span class="text-blue-500">{{ STEPS[currentStep] }}</span>
<span class="text-surface-600">~6 минут</span>
</div>
</div>
<div class="card-content min-h-[400px]">
<div class="kai-section-body flex-1 overflow-y-auto max-h-[65vh] custom-scrollbar">
<Transition name="slide-fade" mode="out-in">
<div v-if="currentStep === 0" key="step-0" class="space-y-5">
<div v-if="currentStep === 0" key="step-0" class="space-y-6 lg:space-y-8 pb-4">
<StepSection title="Базовая информация">
<div class="field md:col-span-2">
<label for="businessNiche" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Ниша бизнеса * </label>
<InputText id="businessNiche" v-model="formData.businessNiche" placeholder="Например: стоматологическая клиника в Алматы" class="w-full" :class="{ 'p-invalid': errors.businessNiche }" />
<small v-if="errors.businessNiche" class="p-error">{{ errors.businessNiche }}</small>
<div class="field md:col-span-2 space-y-1.5">
<label for="businessNiche" class="text-[10px] font-black uppercase tracking-widest text-surface-500 dark:text-surface-400"> Ниша бизнеса * </label>
<InputText id="businessNiche" v-model="formData.businessNiche" placeholder="Например: стоматология в Алматы" class="w-full kai-input text-xs" :class="{ 'p-invalid': errors.businessNiche }" />
<small v-if="errors.businessNiche" class="text-[10px] font-bold text-red-500 uppercase tracking-tight">{{ errors.businessNiche }}</small>
</div>
<div class="field md:col-span-2">
<label for="productName" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Название продукта или бренда * </label>
<InputText id="productName" v-model="formData.productName" placeholder="Например: DentArt Clinic или ТОО 'Стоматология Плюс'" class="w-full" :class="{ 'p-invalid': errors.productName }" />
<small v-if="errors.productName" class="p-error">{{ errors.productName }}</small>
<div class="field md:col-span-2 space-y-1.5">
<label for="productName" class="text-[10px] font-black uppercase tracking-widest text-surface-500 dark:text-surface-400"> Название бренда * </label>
<InputText id="productName" v-model="formData.productName" placeholder="Например: DentArt Clinic" class="w-full kai-input text-xs" :class="{ 'p-invalid': errors.productName }" />
<small v-if="errors.productName" class="text-[10px] font-bold text-red-500 uppercase tracking-tight">{{ errors.productName }}</small>
</div>
<div class="field md:col-span-2">
<label for="goal" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Главная цель маркетинга сейчас * </label>
<InputText id="goal" v-model="formData.goal" placeholder="Например: увеличить поток заявок из Instagram и 2ГИС" class="w-full" :class="{ 'p-invalid': errors.goal }" />
<small v-if="errors.goal" class="p-error">{{ errors.goal }}</small>
<div class="field md:col-span-2 space-y-1.5">
<label for="goal" class="text-[10px] font-black uppercase tracking-widest text-surface-500 dark:text-surface-400"> Главная цель * </label>
<InputText id="goal" v-model="formData.goal" placeholder="Например: увеличить поток заявок" class="w-full kai-input text-xs" :class="{ 'p-invalid': errors.goal }" />
<small v-if="errors.goal" class="text-[10px] font-bold text-red-500 uppercase tracking-tight">{{ errors.goal }}</small>
</div>
</StepSection>
@@ -477,10 +470,10 @@ export default {
<StepSection title="География и чек">
<RadioGroupField v-model="formData.geoScope" label="Где вы работаете?" :options="geoScopeOptions" :error="errors.geoScope" @update:modelValue="formData.geoScope = $event" />
<div v-if="formData.geoScope === 'SINGLE_CITY'" class="field md:col-span-2">
<label for="mainCity" class="block text-sm font-medium mb-2 text-surface-700 dark:text-surface-300"> Основной город * </label>
<Dropdown id="mainCity" v-model="formData.mainCity" :options="KAZAKHSTAN_CITIES" filter placeholder="Выберите город из списка" class="w-full" :class="{ 'p-invalid': errors.mainCity }" />
<small v-if="errors.mainCity" class="p-error">{{ errors.mainCity }}</small>
<div v-if="formData.geoScope === 'SINGLE_CITY'" class="field md:col-span-2 space-y-1.5">
<label for="mainCity" class="text-[10px] font-black uppercase tracking-widest text-surface-500 dark:text-surface-400"> Основной город * </label>
<Dropdown id="mainCity" v-model="formData.mainCity" :options="KAZAKHSTAN_CITIES" filter placeholder="Выберите из базы" class="w-full kai-input text-xs" :class="{ 'p-invalid': errors.mainCity }" />
<small v-if="errors.mainCity" class="text-[10px] font-bold text-red-500 uppercase tracking-tight">{{ errors.mainCity }}</small>
</div>
<div v-if="formData.geoScope === 'MULTI_CITY'" class="field md:col-span-2">
@@ -595,30 +588,99 @@ export default {
</Transition>
</div>
<div class="card-footer flex items-center justify-between gap-4 pt-6 border-t border-surface-200 dark:border-surface-700">
<Button label="Назад" icon="pi pi-arrow-left" severity="secondary" outlined :disabled="currentStep === 0 || submitting" @click="goBack" />
<!-- 3. FOOTER SECTION -->
<div class="p-6 border-t border-surface-800 bg-black/20 flex flex-col sm:flex-row items-center justify-between gap-4">
<Button label="Назад" icon="pi pi-arrow-left" severity="secondary" text class="font-bold text-[11px] uppercase tracking-widest" :disabled="currentStep === 0 || submitting" @click="goBack" />
<Button v-if="currentStep < STEPS.length - 1" label="Далее" icon="pi pi-arrow-right" icon-pos="right" @click="goNext" />
<Button v-else label="Запустить маркетинговый анализ" icon="pi pi-send" icon-pos="right" :loading="submitting" :disabled="submitting" @click="handleSubmit" />
<div class="flex items-center gap-3 w-full sm:w-auto">
<Button v-if="currentStep < STEPS.length - 1" label="Далее" icon="pi pi-arrow-right" icon-pos="right"
class="w-full sm:w-48 bg-blue-600 hover:bg-blue-500 border-none font-bold text-[11px] uppercase tracking-widest h-10 shadow-lg shadow-blue-900/20" @click="goNext" />
<Button v-else label="Запустить анализ" icon="pi pi-bolt" icon-pos="right"
class="w-full sm:w-64 bg-emerald-600 hover:bg-emerald-500 border-none font-black text-[11.5px] uppercase tracking-widest h-11"
:loading="submitting" :disabled="submitting" @click="handleSubmit" />
</div>
</div>
</div>
<!-- AI Status Bar -->
<div class="mt-4 flex items-center justify-center gap-4 text-[9px] font-black uppercase tracking-[0.2em] text-surface-500">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_var(--kai-green)]"></span>
KAI Engine: Ready
<span class="w-px h-3 bg-surface-800"></span>
Secure Input Enabled
</div>
</div>
</div>
</template>
<style scoped>
.kai-input {
background: rgba(255, 255, 255, 0.03) !important;
border: 1px solid rgba(45, 123, 255, 0.15) !important;
border-radius: 12px !important;
color: var(--kai-txt) !important;
transition: all 0.3s;
}
.kai-input:focus {
background: rgba(255, 255, 255, 0.06) !important;
border-color: var(--kai-blue) !important;
box-shadow: 0 0 15px rgba(45, 123, 255, 0.1) !important;
}
.kai-input::placeholder {
color: var(--kai-txt3) !important;
font-size: 11px;
}
.custom-scrollbar::-webkit-scrollbar {
width: 4px;
}
.custom-scrollbar::-webkit-scrollbar-track {
background: transparent;
}
.custom-scrollbar::-webkit-scrollbar-thumb {
background: var(--kai-border);
border-radius: 999px;
}
.slide-fade-enter-active,
.slide-fade-leave-active {
transition: all 0.25s ease;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.slide-fade-enter-from {
opacity: 0;
transform: translateX(20px);
transform: translateY(10px);
}
.slide-fade-leave-to {
opacity: 0;
transform: translateX(-20px);
transform: translateY(-10px);
}
:deep(.p-dropdown), :deep(.p-multiselect) {
background: rgba(255, 255, 255, 0.03) !important;
border: 1px solid rgba(45, 123, 255, 0.15) !important;
border-radius: 12px;
}
:deep(.p-dropdown-label), :deep(.p-multiselect-label) {
font-size: 11.5px !important;
color: var(--kai-txt) !important;
padding: 10px 14px !important;
}
:deep(.p-inputtext) {
font-size: 11.5px !important;
padding: 10px 14px;
}
:deep(.p-multiselect-token) {
background: rgba(var(--kai-blue-rgb), 0.15) !important;
color: var(--kai-blue) !important;
font-size: 10px !important;
font-weight: 700 !important;
border-radius: 6px !important;
}
</style>
@@ -6,58 +6,297 @@ const props = defineProps({ id: String, data: Object });
const fmtPct = (n) => n != null ? parseFloat(n).toFixed(1) + '%' : '—';
const behaviors = computed(() => [
{ icon:'🏷', label:'Акции и скидки', val: props.data?.promoUsagePercent || 0, desc:'Конкурентов используют акции', color:'#FF7A2D', tip: 'Процент успешных конкурентов, активно использующих промо-предложения' },
{ icon:'💰', label:'Открытые цены', val: props.data?.priceVisibilityPercent || 0, desc:'Указывают цены открыто (остальные — в Direct)', color:'#A855F7', tip: 'Процент компаний, которые пишут цены прямо в постах, а не просят писать в Директ' },
{ icon:'📅', label:'Прямое бронирование', val: props.data?.directBookingPercent || 0, desc:остов заканчиваются призывом записаться', color:'#2D7BFF', tip: 'Наличие прямой ссылки на бронирование, заявку или номер телефона' },
{ icon:'pi-ticket', label:'Акции и скидки', val: props.data?.promoUsagePercent || 0, desc:'Конкурентов используют акции', color:'var(--kai-orange)', tip: 'Процент успешных конкурентов с промо-предложениями' },
{ icon:'pi-dollar', label:'Открытые цены', val: props.data?.priceVisibilityPercent || 0, desc:'Указывают цены открыто', color:'var(--kai-purple)', tip: 'Процент компаний, пишущих цены в постах' },
{ icon:'pi-calendar-plus', label:'Прямое бронирование', val: props.data?.directBookingPercent || 0, desc:'Призывы к записи/брони', color:'var(--kai-blue)', tip: 'Наличие прямой ссылки на бронирование/заявку' },
]);
</script>
<template>
<SectionWrapper :id="id" title="Поведение рынка" subtitle="Как конкуренты общаются с клиентами" num="07">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;">
<!-- Behavior bars -->
<div style="display:flex;flex-direction:column;gap:20px;">
<div v-for="b in behaviors" :key="b.label">
<div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:8px;">
<div style="display:flex;align-items:center;gap:8px;">
<span>{{ b.icon }}</span>
<span style="font-size:13px;font-weight:600;color:rgba(255,255,255,0.8);">
<SectionWrapper :id="id" title="Поведение рынка" subtitle="Анализ методов взаимодействия конкурентов с аудиторией" num="7">
<div class="behavior-grid">
<!-- LEFT: BARS -->
<div class="behavior-bars-col">
<div class="bars-container">
<div v-for="b in behaviors" :key="b.label" class="behavior-item">
<div class="item-header">
<div class="item-title-group">
<div class="item-icon-box" :style="{ color: b.color, background: b.color + '15' }">
<i class="pi" :class="b.icon"></i>
</div>
<div class="item-info">
<span class="item-label">
{{ b.label }}
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="b.tip"></i>
</span>
<span class="item-desc">{{ b.desc }}</span>
</div>
</div>
<span class="item-val" :style="{ color: b.color }">{{ fmtPct(b.val) }}</span>
</div>
<div class="kai-progress-track">
<div class="kai-progress-fill" :style="{ width: b.val+'%', background: b.color }" />
</div>
<span style="font-family:Unbounded,sans-serif;font-size:16px;font-weight:900;" :style="{ color: b.color }">{{ fmtPct(b.val) }}</span>
</div>
<div class="kai-progress-track" style="height:10px;"><div class="kai-progress-fill" :style="{ width: b.val+'%', background: b.color }" /></div>
<div style="font-size:11px;color:rgba(255,255,255,0.4);margin-top:4px;">{{ b.desc }}</div>
</div>
<!-- Cycle days -->
<div style="padding:14px 16px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.07);border-radius:12px;display:flex;align-items:center;gap:14px;">
<span style="font-size:24px;">📆</span>
<div>
<div style="font-size:11px;color:rgba(255,255,255,0.4);font-weight:600;text-transform:uppercase;letter-spacing:.05em;">
<!-- Cycle days box -->
<div class="cycle-box">
<div class="cycle-icon">
<i class="pi pi-history"></i>
</div>
<div class="cycle-text">
<div class="cycle-label">
Цикл публикаций
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Среднее время, проходящее между постами конкурентов'"></i>
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Средний интервал между постами конкурентов'"></i>
</div>
<div style="font-family:Unbounded,sans-serif;font-size:22px;font-weight:900;color:rgba(255,255,255,0.9);">{{ data?.avgCycleDays || '—' }} <span style="font-size:13px;font-weight:400;">дней</span></div>
<div class="cycle-val">
{{ data?.avgCycleDays || '—' }}
<span class="unit">дней</span>
</div>
</div>
<div class="cycle-badge" :class="{ ok: parseFloat(data?.avgCycleDays) <= 3 }">
{{ parseFloat(data?.avgCycleDays) <= 3 ? 'HIGH ACTIVITY' : 'MODERATE' }}
</div>
</div>
</div>
<!-- CTA bubbles -->
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:14px;">Частые призывы к действию (CTA)</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<div v-for="(cta, i) in (data?.commonCta || [])" :key="i" class="kai-cta-bubble" :style="{ fontSize: Math.max(16 - i * 2, 11) + 'px' }">
<span style="opacity:.5;margin-right:6px;">#{{ i+1 }}</span> "{{ cta }}"
<!-- RIGHT: CTA LIST -->
<div class="behavior-cta-col">
<h4 class="cta-title">Популярные призывы (CTA)</h4>
<div class="cta-cloud">
<div v-for="(cta, i) in (data?.commonCta || [])" :key="i" class="cta-tag">
<span class="cta-idx">#{{ i+1 }}</span>
<span class="cta-text">"{{ cta }}"</span>
</div>
<div v-if="!data?.commonCta?.length" style="color:rgba(255,255,255,0.3);font-size:13px;font-style:italic;text-align:center;padding:20px;">Нет данных</div>
<div v-if="!data?.commonCta?.length" class="empty-cta">Нет данных</div>
</div>
<div style="margin-top:16px;padding:10px 14px;background:rgba(0,212,139,0.06);border:1px solid rgba(0,212,139,0.15);border-radius:10px;font-size:11px;color:rgba(0,212,139,0.8);">
💡 Используйте эти формулировки они уже проверены рынком
<div class="cta-suggestion">
<i class="pi pi-lightbulb mr-2 text-green-500" />
<span>Эти формулировки проверены рынком и имеют высокий отклик.</span>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.behavior-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
align-items: start;
}
.behavior-bars-col {
display: flex;
flex-direction: column;
gap: 24px;
}
.bars-container {
display: flex;
flex-direction: column;
gap: 20px;
padding: 20px;
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 16px;
}
.behavior-item {
display: flex;
flex-direction: column;
gap: 12px;
}
.item-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.item-title-group {
display: flex;
align-items: center;
gap: 12px;
}
.item-icon-box {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.item-icon-box i {
font-size: 14px;
}
.item-info {
display: flex;
flex-direction: column;
}
.item-label {
font-size: 11.5px;
font-weight: 700;
color: var(--kai-txt);
display: flex;
align-items: center;
gap: 6px;
}
.item-desc {
font-size: 10px;
color: var(--kai-txt3);
opacity: 0.6;
}
.item-val {
font-family: 'Unbounded', sans-serif;
font-size: 16px;
font-weight: 800;
}
/* Cycle Box */
.cycle-box {
display: flex;
align-items: center;
gap: 20px;
padding: 16px 20px;
background: rgba(0,0,0,0.15);
border: 1px solid var(--kai-border);
border-radius: 16px;
}
.cycle-icon {
width: 40px;
height: 40px;
border-radius: 12px;
background: rgba(var(--kai-blue-rgb), 0.1);
color: var(--kai-blue);
display: flex;
align-items: center;
justify-content: center;
font-size: 18px;
}
.cycle-text {
flex: 1;
}
.cycle-label {
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.05em;
margin-bottom: 2px;
}
.cycle-val {
font-family: 'Unbounded', sans-serif;
font-size: 20px;
font-weight: 800;
color: var(--kai-txt);
}
.cycle-val .unit {
font-size: 12px;
font-weight: 400;
color: var(--kai-txt3);
margin-left: 4px;
}
.cycle-badge {
font-size: 8px;
font-weight: 900;
padding: 4px 8px;
border-radius: 4px;
background: var(--kai-border);
color: var(--kai-txt3);
}
.cycle-badge.ok {
background: rgba(var(--kai-green-rgb), 0.1);
color: var(--kai-green);
}
/* CTA Col */
.cta-title {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
margin-bottom: 24px;
}
.cta-cloud {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 24px;
}
.cta-tag {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
padding: 10px 16px;
border-radius: 10px;
display: flex;
align-items: center;
gap: 12px;
transition: all 0.3s;
}
.cta-tag:hover {
border-color: var(--kai-blue);
background: rgba(var(--kai-blue-rgb), 0.05);
}
.cta-idx {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
color: var(--kai-blue);
opacity: 0.5;
}
.cta-text {
font-size: 12px;
font-weight: 700;
color: var(--kai-txt2);
}
.cta-suggestion {
padding: 12px 16px;
background: rgba(var(--kai-green-rgb), 0.05);
border: 1px solid rgba(var(--kai-green-rgb), 0.15);
border-radius: 10px;
font-size: 10px;
color: var(--kai-green);
opacity: 0.8;
display: flex;
align-items: center;
}
.empty-cta {
text-align: center;
color: var(--kai-txt3);
padding: 32px;
font-size: 11px;
}
@media (max-width: 992px) {
.behavior-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -10,20 +10,21 @@ const factors = computed(() => props.data?.contributingFactors || []);
const ciiColor = computed(() => {
const v = cii.value;
if (v <= 25) return '#00D48B';
if (v <= 50) return '#F59E0B';
if (v <= 75) return '#FF7A2D';
return '#EF4444';
});
const ciiDesc = computed(() => {
const v = cii.value;
if (v <= 25) return '✅ Хорошие условия для старта. Рынок не перегружен.';
if (v <= 50) return '⚡ Конкуренция есть, но место найдётся. Важен контент.';
if (v <= 75) return '🔥 Рынок активный. Нужна чёткая стратегия отличия.';
return '⚠️ Высокая конкуренция. Требуется серьёзное SMM-присутствие.';
if (v <= 25) return 'var(--kai-green)';
if (v <= 50) return 'var(--kai-yellow)';
if (v <= 75) return 'var(--kai-orange)';
return 'var(--kai-red)';
});
const FACTOR_ICONS = ['👥','📅','❤️','⭐'];
const ciiDesc = computed(() => {
const v = cii.value;
if (v <= 25) return 'Хорошие условия для старта. Рынок не перегружен.';
if (v <= 50) return 'Конкуренция есть, но место найдётся. Важен контент.';
if (v <= 75) return 'Рынок активный. Нужна чёткая стратегия отличия.';
return 'Высокая интенсивность. Требуется сильное присутствие.';
});
const FACTOR_ICONS = ['pi-users', 'pi-calendar', 'pi-percentage', 'pi-star'];
// SVG half-gauge
const arcD = computed(() => {
@@ -33,49 +34,220 @@ const arcD = computed(() => {
const r = 80;
const x = 100 + r * Math.cos(rad);
const y = 100 + r * Math.sin(rad);
// A half-circle never exceeds 180 degrees, so the large-arc-flag should always be 0
const lg = 0;
return `M 20 100 A 80 80 0 ${lg} 1 ${x} ${y}`;
});
</script>
<template>
<SectionWrapper :id="id" title="Индекс интенсивности конкуренции" subtitle="Насколько сложно войти и удержаться в этой нише" num="05">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:28px;align-items:start;">
<SectionWrapper :id="id" title="Интенсивность рынка" subtitle="Комплексный индекс CII (Competition Intensity Index)" num="5">
<div class="cii-grid">
<!-- Half gauge -->
<div class="kai-cii-ring-wrap">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:12px;text-align:center;">
Общий индекс CII
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Competition Intensity Index (СII) комплексный показатель жесткости конкуренции, рассчитанный ИИ. Учитывает плотность игроков, их активность и бюджеты.'"></i>
<!-- GAUGE COL -->
<div class="cii-gauge-col">
<div class="cii-header-label">
Overall CII Score
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Учитывает плотность игроков, частоту контента и уровень активности.'"></i>
</div>
<svg width="200" height="120" viewBox="0 0 200 120">
<path d="M 20 100 A 80 80 0 0 1 180 100" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="14" stroke-linecap="round"/>
<path :d="arcD" fill="none" :stroke="ciiColor" stroke-width="14" stroke-linecap="round"/>
<!-- Zones -->
<text x="24" y="118" fill="#00D48B" font-size="8" font-family="Onest">LOW</text>
<text x="172" y="118" fill="#EF4444" font-size="8" font-family="Onest" text-anchor="end">HIGH</text>
<div class="cii-svg-wrap">
<svg class="cii-svg" viewBox="0 0 200 120">
<path d="M 20 100 A 80 80 0 0 1 180 100" class="cii-track"/>
<path :d="arcD" :stroke="ciiColor" class="cii-fill"/>
<text x="30" y="118" class="cii-meta-label">LOW</text>
<text x="170" y="118" class="cii-meta-label text-end">HIGH</text>
</svg>
<div class="kai-cii-num" :style="{ color: ciiColor }">{{ cii }}</div>
<div class="kai-cii-label" :style="{ color: ciiColor }">{{ label }}</div>
<div class="kai-cii-desc">{{ ciiDesc }}</div>
<div class="cii-value-display">
<div class="cii-num" :style="{ color: ciiColor }">{{ cii }}</div>
<div class="cii-label-text" :style="{ color: ciiColor }">{{ label }}</div>
</div>
</div>
<!-- Factors -->
<div style="display:flex;flex-direction:column;gap:12px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:4px;">Ключевые факторы</div>
<div v-for="(f, i) in factors" :key="i" class="kai-factor">
<div style="display:flex;align-items:center;gap:10px;margin-bottom:10px;">
<span style="font-size:20px;">{{ FACTOR_ICONS[i % FACTOR_ICONS.length] }}</span>
<div>
<div class="kai-factor-label">{{ ['Плотность игроков','Активность контента','Вовлечённость','Репутационный уровень'][i] || 'Фактор ' + (i+1) }}</div>
<div class="kai-factor-val">{{ f }}</div>
<div class="cii-description">
<i class="pi pi-shield text-[10px] mr-2" :style="{ color: ciiColor }" />
{{ ciiDesc }}
</div>
</div>
<div class="kai-progress-track"><div class="kai-progress-fill" :style="{ width: [35,25,20,20][i % 4]+'%', background: ciiColor }" /></div>
<!-- FACTORS COL -->
<div class="cii-factors-col">
<h4 class="cii-subtitle">Ключевые факторы</h4>
<div class="factors-list">
<div v-for="(f, i) in factors" :key="i" class="factor-item">
<div class="factor-header">
<div class="factor-icon-box" :style="{ color: ciiColor, background: ciiColor + '10' }">
<i class="pi" :class="FACTOR_ICONS[i % FACTOR_ICONS.length]"></i>
</div>
<div v-if="!factors.length" style="color:rgba(255,255,255,0.3);font-size:13px;text-align:center;padding:20px 0;">Нет данных</div>
<div class="factor-info">
<div class="factor-name">{{ ['Плотность','Активность','Вовлечённость','Репутация'][i] || 'Показатель' }}</div>
<div class="factor-val">{{ f }}</div>
</div>
</div>
<div class="kai-progress-track">
<div class="kai-progress-fill" :style="{ width: [35,25,20,20][i % 4]+'%', background: ciiColor }" />
</div>
</div>
<div v-if="!factors.length" class="empty-state">Нет данных</div>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.cii-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
align-items: start;
}
.cii-gauge-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.cii-header-label {
font-family: 'Unbounded', sans-serif;
font-size: 9.5px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.1em;
}
.cii-svg-wrap {
position: relative;
width: 220px;
}
.cii-track {
fill: none;
stroke: var(--kai-border);
stroke-width: 14;
stroke-linecap: round;
}
.cii-fill {
fill: none;
stroke-width: 14;
stroke-linecap: round;
transition: all 1s ease-out;
}
.cii-meta-label {
font-size: 8px;
font-weight: 900;
fill: var(--kai-txt3);
letter-spacing: 0.05em;
}
.cii-value-display {
position: absolute;
top: 60px;
left: 0;
right: 0;
text-align: center;
}
.cii-num {
font-family: 'Unbounded', sans-serif;
font-size: 32px;
font-weight: 900;
line-height: 1;
}
.cii-label-text {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
}
.cii-description {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
padding: 12px 16px;
border-radius: 12px;
font-size: 11px;
color: var(--kai-txt2);
line-height: 1.5;
text-align: center;
max-width: 280px;
}
/* Factors */
.cii-subtitle {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
margin-bottom: 24px;
}
.factors-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.factor-item {
background: rgba(255,255,255,0.02);
padding: 12px;
border-radius: 12px;
border: 1px solid var(--kai-border);
}
.factor-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.factor-icon-box {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
}
.factor-icon-box i {
font-size: 14px;
}
.factor-name {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.05em;
margin-bottom: 2px;
}
.factor-val {
font-size: 11.5px;
font-weight: 700;
color: var(--kai-txt);
}
.empty-state {
color: var(--kai-txt3);
font-size: 11px;
text-align: center;
padding: 20px 0;
}
@media (max-width: 768px) {
.cii-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -20,7 +20,7 @@ const toggleRow = (name) => { expanded.value = expanded.value === name ? null :
const fmtNum = (n) => n != null ? Number(n).toLocaleString('ru-RU') : '—';
const fmtPct = (n) => n != null ? parseFloat(n).toFixed(2) + '%' : '—';
const erColor = (er) => parseFloat(er) >= 6 ? '#00D48B' : parseFloat(er) >= 3 ? '#F59E0B' : '#EF4444';
const erColor = (er) => parseFloat(er) >= 6 ? 'var(--kai-green)' : parseFloat(er) >= 3 ? 'var(--kai-yellow)' : 'var(--kai-red)';
const PLAT_COLORS = { Instagram:'#E1306C', TikTok:'#69C9D0', Telegram:'#2AABEE', '2GIS':'#00B140', YouTube:'#FF0000' };
const platColor = (p) => PLAT_COLORS[p] || '#808080';
@@ -39,100 +39,310 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
</script>
<template>
<SectionWrapper :id="id" title="Карта конкурентов" subtitle="Кто ваши главные конкуренты и где они стоят" num="03">
<SectionWrapper :id="id" title="Карта конкурентов" subtitle="Позиционирование игроков: Активность vs Вовлечённость" num="3">
<!-- Scatter plot (SVG) -->
<div style="background:rgba(255,255,255,0.02);border:1px solid rgba(45,123,255,0.1);border-radius:12px;padding:16px;margin-bottom:20px;overflow:hidden;position:relative;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.35);margin-bottom:12px;">
Активность (посты/мес) vs Вовлечённость (ER %)
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Чем правее точка тем чаще бренд публикует контент. Чем выше точка тем активнее аудитория реагирует. Лидеры (справа вверху) делают много постов и получают хороший отклик.'"></i>
<!-- Scatter plot -->
<div class="competitor-plot-card">
<div class="plot-header">
<span class="plot-axis-label">Активность (посты/мес) </span>
<span class="plot-axis-label vertical">Вовлечённость (ER %) </span>
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Лидеры (справа вверху) делают много постов и получают высокий отклик.'"></i>
</div>
<!-- Leader zone label -->
<div style="position:absolute;top:36px;right:24px;font-size:10px;font-weight:700;color:rgba(45,123,255,0.5);border:1px dashed rgba(45,123,255,0.2);padding:4px 10px;border-radius:6px;">ЛИДЕРЫ</div>
<svg style="width:100%;height:240px;" :viewBox="`0 0 500 220`" preserveAspectRatio="none">
<div class="plot-leader-zone">ЛИДЕРЫ</div>
<div class="plot-container">
<svg class="plot-svg" :viewBox="`0 0 500 220`" preserveAspectRatio="none">
<!-- Grid -->
<line x1="50" y1="10" x2="50" y2="200" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<line x1="50" y1="200" x2="490" y2="200" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<line x1="50" y1="10" x2="50" y2="200" class="plot-grid-line"/>
<line x1="50" y1="200" x2="490" y2="200" class="plot-grid-line"/>
<!-- Leader quadrant -->
<rect x="270" y="10" width="220" height="100" fill="rgba(45,123,255,0.03)" rx="4"/>
<rect x="270" y="10" width="220" height="100" class="plot-leader-rect" rx="8"/>
<!-- Data points -->
<g v-for="(pt, i) in scatter" :key="i">
<g v-for="(pt, i) in scatter" :key="i" class="plot-point-group">
<circle
:cx="50 + (pt.x / maxX) * 440"
:cy="195 - (pt.y / maxY) * 185"
:r="Math.max(pt.r, 6)"
:r="Math.max(pt.r, 5)"
:fill="pt.color"
fill-opacity="0.8"
style="cursor:pointer;"
class="plot-point"
/>
<text
:x="50 + (pt.x / maxX) * 440"
:y="195 - (pt.y / maxY) * 185 - Math.max(pt.r, 6) - 4"
text-anchor="middle"
fill="rgba(255,255,255,0.6)"
font-size="9"
font-family="Onest,sans-serif"
>{{ pt.name.slice(0, 12) }}</text>
:y="195 - (pt.y / maxY) * 185 - Math.max(pt.r, 5) - 6"
class="plot-point-text"
>{{ pt.name }}</text>
</g>
</svg>
</div>
</div>
<!-- Table -->
<div style="overflow-x:auto;">
<div class="table-container">
<table class="kai-table">
<thead><tr>
<th>#</th><th>Бренд</th><th>Платформа</th><th>Подписчики</th>
<th>ER <i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Вовлечённость (Engagement Rate)'"></i></th>
<thead>
<tr>
<th width="40">#</th>
<th>Бренд</th>
<th>Платформа</th>
<th>Подписчики</th>
<th>ER</th>
<th> Рейтинг</th>
<th>Постов/мес <i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Среднее количество публикаций в месяц'"></i></th>
<th></th>
</tr></thead>
<th>Контент</th>
<th width="40"></th>
</tr>
</thead>
<tbody>
<template v-for="(c, i) in sorted" :key="c.name">
<tr :style="i < 3 ? { borderLeft: `3px solid ${ ['#F59E0B','#9CA3AF','#CD7F32'][i] }` } : {}">
<td><span style="font-family:Unbounded,sans-serif;font-size:11px;font-weight:700;color:rgba(255,255,255,0.3);">{{ String(i+1).padStart(2,'0') }}</span></td>
<tr :class="{ 'top-row': i < 3 }" :style="i < 3 ? { '--top-color': ['#F59E0B','#939393','#CD7F32'][i] } : {}">
<td class="row-num">{{ String(i+1).padStart(2,'0') }}</td>
<td>
<div style="display:flex;align-items:center;gap:10px;">
<div :style="{ width:'30px', height:'30px', borderRadius:'8px', background: platColor(c.platform)+'20', border:`1px solid ${platColor(c.platform)}40`, display:'flex', alignItems:'center', justifyContent:'center', fontSize:'14px', flexShrink:0 }">🏢</div>
<span style="font-weight:700;">{{ c.name }}</span>
<div class="brand-cell">
<div class="brand-avatar" :style="{ background: platColor(c.platform) + '20', color: platColor(c.platform), borderColor: platColor(c.platform) + '40' }">
<i class="pi pi-building text-[11px]" />
</div>
<span class="brand-name">{{ c.name }}</span>
</div>
</td>
<td><span class="kai-plat-badge" :style="{ background: platColor(c.platform)+'20', color: platColor(c.platform), border: `1px solid ${platColor(c.platform)}40` }">{{ c.platform }}</span></td>
<td style="font-weight:600;">{{ fmtNum(c.followersVal) }}</td>
<td><span style="font-weight:700;" :style="{ color: erColor(c.erVal) }">{{ fmtPct(c.erVal) }}</span></td>
<td><span style="color:#F59E0B;font-weight:700;"> {{ (c.ratingVal).toFixed(1) }}</span></td>
<td style="font-weight:600;">{{ c.postsVal }}</td>
<td><span class="kai-plat-badge" :style="{ background: platColor(c.platform)+'20', color: platColor(c.platform), border: `1px solid ${platColor(c.platform)}20` }">{{ c.platform }}</span></td>
<td class="font-bold">{{ fmtNum(c.followersVal) }}</td>
<td class="font-bold" :style="{ color: erColor(c.erVal) }">{{ fmtPct(c.erVal) }}</td>
<td class="font-bold text-yellow-500"> {{ (c.ratingVal).toFixed(1) }}</td>
<td class="font-bold">{{ c.postsVal }} <small class="text-surface-500 ml-1">/ мес</small></td>
<td>
<button class="kai-expand-btn" @click="toggleRow(c.name)">
<i :class="['pi', expanded===c.name ? 'pi-chevron-up' : 'pi-chevron-down']" style="font-size:12px;"></i>
<button class="expand-btn" :class="{ active: expanded === c.name }" @click="toggleRow(c.name)">
<i class="pi pi-chevron-down" />
</button>
</td>
</tr>
<!-- Expanded row -->
<tr v-if="expanded === c.name">
<td colspan="8" class="kai-expand-row" style="padding:0;">
<div class="kai-expand-inner">
<div>
<div class="kai-sw-title" style="color:#00D48B;"> Сильные стороны</div>
<ul class="kai-sw-list">
<li v-for="(s, si) in (c.strengths||[])" :key="si"><span class="kai-sw-dot green"/><span>{{ s }}</span></li>
<li v-if="!c.strengths?.length" style="color:rgba(255,255,255,0.3);font-style:italic;">Нет данных</li>
<!-- Expanded content -->
<tr v-if="expanded === c.name" class="expanded-row">
<td colspan="8">
<div class="expand-content">
<div class="sw-grid">
<div class="sw-col">
<div class="sw-title green">Сильные стороны</div>
<ul class="sw-list">
<li v-for="(s, si) in (c.strengths||[])" :key="si">{{ s }}</li>
<li v-if="!c.strengths?.length" class="empty-sw">Нет данных</li>
</ul>
</div>
<div>
<div class="kai-sw-title" style="color:#EF4444;"> Слабые стороны</div>
<ul class="kai-sw-list">
<li v-for="(w, wi) in (c.weaknesses||[])" :key="wi"><span class="kai-sw-dot red"/><span>{{ w }}</span></li>
<li v-if="!c.weaknesses?.length" style="color:rgba(255,255,255,0.3);font-style:italic;">Нет данных</li>
<div class="sw-col">
<div class="sw-title red">Слабые стороны</div>
<ul class="sw-list">
<li v-for="(w, wi) in (c.weaknesses||[])" :key="wi">{{ w }}</li>
<li v-if="!c.weaknesses?.length" class="empty-sw">Нет данных</li>
</ul>
</div>
</div>
</div>
</td>
</tr>
</template>
<tr v-if="!sorted.length"><td colspan="8" style="text-align:center;color:rgba(255,255,255,0.3);padding:32px;">Нет данных</td></tr>
<tr v-if="!sorted.length"><td colspan="8" class="empty-table">Нет данных</td></tr>
</tbody>
</table>
</div>
</SectionWrapper>
</template>
<style scoped>
.competitor-plot-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 20px;
padding: 24px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.plot-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 24px;
}
.plot-axis-label {
font-size: 9.5px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--kai-txt3);
}
.plot-leader-zone {
position: absolute;
top: 60px;
right: 40px;
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
color: var(--kai-blue);
opacity: 0.4;
letter-spacing: 0.2em;
pointer-events: none;
}
.plot-container {
padding-left: 20px;
}
.plot-grid-line {
stroke: var(--kai-border);
stroke-width: 1;
}
.plot-leader-rect {
fill: rgba(var(--kai-blue-rgb), 0.05);
}
.plot-point {
fill-opacity: 0.8;
stroke: rgba(255,255,255,0.2);
stroke-width: 1;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.plot-point-group:hover .plot-point {
fill-opacity: 1;
r: 10;
stroke-width: 2;
filter: drop-shadow(0 0 8px currentColor);
}
.plot-point-text {
font-size: 8px;
font-weight: 700;
fill: var(--kai-txt2);
text-anchor: middle;
pointer-events: none;
}
/* Table Stylings */
.table-container {
border-radius: 16px;
overflow: hidden;
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
}
.top-row {
background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.03), transparent);
border-left: 3px solid var(--top-color);
}
.row-num {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
color: var(--kai-txt3);
opacity: 0.6;
}
.brand-cell {
display: flex;
align-items: center;
gap: 12px;
}
.brand-avatar {
width: 28px;
height: 28px;
border-radius: 8px;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.brand-name {
font-size: 12px;
font-weight: 700;
color: var(--kai-txt);
}
.expand-btn {
background: transparent;
border: none;
color: var(--kai-txt3);
cursor: pointer;
padding: 4px;
transition: all 0.3s;
}
.expand-btn.active {
transform: rotate(180deg);
color: var(--kai-blue);
}
.expanded-row td {
padding: 0 !important;
background: rgba(0,0,0,0.2);
border-bottom: 1px solid var(--kai-border);
}
.expand-content {
padding: 24px 32px;
}
.sw-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.sw-title {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 16px;
}
.sw-title.green { color: var(--kai-green); }
.sw-title.red { color: var(--kai-red); }
.sw-list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 8px;
}
.sw-list li {
font-size: 11.5px;
color: var(--kai-txt2);
display: flex;
align-items: flex-start;
gap: 10px;
}
.sw-list li::before {
content: '→';
color: var(--kai-txt3);
font-size: 10px;
margin-top: 1px;
}
.empty-sw {
color: var(--kai-txt3) !important;
font-style: italic;
font-size: 11px !important;
}
.empty-table {
text-align: center;
color: var(--kai-txt3);
padding: 40px;
font-size: 12px;
}
</style>
@@ -4,14 +4,13 @@ import { computed } from 'vue';
const props = defineProps({ id: String, data: Array });
const ICONS = ['👥','🗂','','📈','💲','🎬',''];
const ICONS = ['pi-users', 'pi-folder', 'pi-star', 'pi-chart-line', 'pi-dollar', 'pi-video', 'pi-bolt'];
const conclusions = computed(() => {
const arr = props.data || [];
return arr.map((text, i) => {
// try to guess the theme based on text
const t = text.toLowerCase();
let themeIdx = 1; // default layout/content
let themeIdx = 1;
if (t.includes('конкурент') || t.includes('игрок')) themeIdx = 0;
else if (t.includes('репутац') || t.includes('отзыв') || t.includes('рейтинг')) themeIdx = 2;
else if (t.includes('спрос') || t.includes('пик')) themeIdx = 3;
@@ -25,23 +24,139 @@ const conclusions = computed(() => {
</script>
<template>
<SectionWrapper :id="id" title="Выводы анализа" subtitle="7 ключевых фактов о вашем рынке — простым языком" num="10">
<div style="display:grid;grid-template-columns:repeat(auto-fit, minmax(320px, 1fr));gap:16px;">
<SectionWrapper :id="id" title="Выводы анализа" subtitle="Стратегическое резюме рынка на основе собранных данных" num="10">
<div class="conclusions-grid">
<div v-for="(c, i) in conclusions" :key="i" class="kai-conclusion-card" :style="i === conclusions.length - 1 && conclusions.length % 2 !== 0 ? 'grid-column: 1 / -1;' : ''">
<div style="display:flex;align-items:flex-start;gap:16px;">
<div style="width:40px;height:40px;border-radius:10px;background:rgba(45,123,255,0.1);border:1px solid rgba(45,123,255,0.2);display:flex;align-items:center;justify-content:center;font-size:20px;flex-shrink:0;">
{{ c.icon }}
<div v-for="(c, i) in conclusions" :key="i" class="conclusion-card" :class="{ 'full-width': i === conclusions.length - 1 && conclusions.length % 2 !== 0 }">
<div class="card-inner">
<div class="card-icon-box">
<i class="pi" :class="c.icon"></i>
</div>
<div style="flex:1;">
<div style="font-size:10px;font-family:Unbounded,sans-serif;font-weight:700;color:rgba(45,123,255,0.7);margin-bottom:6px;letter-spacing:.05em;">ВЫВОД {{ c.number }}</div>
<div style="font-size:14px;line-height:1.5;color:rgba(255,255,255,0.9);font-weight:500;">{{ c.text }}</div>
<div class="card-content">
<div class="card-header">
<span class="card-label">INSIGHT {{ c.number }}</span>
<span class="card-dot"></span>
</div>
<div class="card-text">{{ c.text }}</div>
</div>
</div>
</div>
<div v-if="!conclusions.length" style="grid-column: 1 / -1;text-align:center;color:rgba(255,255,255,0.3);padding:40px;">Выводы формируются...</div>
<div v-if="!conclusions.length" class="empty-state">
<i class="pi pi-spin pi-spinner mr-2"></i> Формирование аналитических выводов...
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.conclusions-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(340px, 1fr));
gap: 16px;
}
.conclusion-card {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
.conclusion-card.full-width {
grid-column: 1 / -1;
}
.card-inner {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
padding: 20px 24px;
border-radius: 20px;
display: flex;
gap: 20px;
height: 100%;
}
.conclusion-card:hover .card-inner {
border-color: var(--kai-blue);
background: var(--kai-card-hover);
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(0,0,0,0.2);
}
.card-icon-box {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(var(--kai-blue-rgb), 0.1);
border: 1px solid rgba(var(--kai-blue-rgb), 0.2);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
transition: all 0.3s;
}
.card-icon-box i {
font-size: 18px;
color: var(--kai-blue);
}
.conclusion-card:hover .card-icon-box {
background: var(--kai-blue);
}
.conclusion-card:hover .card-icon-box i {
color: #fff;
}
.card-content {
flex: 1;
}
.card-header {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.card-label {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
color: var(--kai-blue);
letter-spacing: 0.1em;
opacity: 0.8;
}
.card-dot {
width: 4px;
height: 4px;
background: var(--kai-blue);
border-radius: 50%;
opacity: 0.3;
}
.card-text {
font-size: 11.5px;
line-height: 1.6;
color: var(--kai-txt);
font-weight: 500;
}
.empty-state {
grid-column: 1 / -1;
text-align: center;
color: var(--kai-txt3);
padding: 64px 0;
font-size: 12px;
font-weight: 600;
border: 1px dashed var(--kai-border);
border-radius: 20px;
}
@media (max-width: 768px) {
.conclusions-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -7,11 +7,11 @@ const fmtPct = (n) => n != null ? parseFloat(n).toFixed(1) + '%' : '—';
const fmtNum = (n) => n != null ? Number(n).toLocaleString('ru-RU') : '—';
const SEGMENTS = [
{ key: 'demoContentPercent', label: 'Демонстрация', color: '#2D7BFF' },
{ key: 'expertContentPercent', label: 'Экспертный', color: '#00D48B' },
{ key: 'salesContentPercent', label: 'Продающий', color: '#FF7A2D' },
{ key: 'reviewsContentPercent', label: 'Отзывы', color: '#A855F7' },
{ key: 'engagementContentPercent', label: 'Вовлекающий', color: '#F59E0B' },
{ key: 'demoContentPercent', label: 'Демонстрация', color: 'var(--kai-blue)' },
{ key: 'expertContentPercent', label: 'Экспертный', color: 'var(--kai-green)' },
{ key: 'salesContentPercent', label: 'Продающий', color: 'var(--kai-orange)' },
{ key: 'reviewsContentPercent', label: 'Отзывы', color: 'var(--kai-purple)' },
{ key: 'engagementContentPercent', label: 'Вовлекающий', color: 'var(--kai-yellow)' },
];
const segments = computed(() => SEGMENTS.map(s => ({ ...s, value: props.data?.[s.key] || 0 })).filter(s => s.value > 0));
@@ -19,7 +19,7 @@ const segments = computed(() => SEGMENTS.map(s => ({ ...s, value: props.data?.[s
// SVG donut chart
const donut = computed(() => {
const total = segments.value.reduce((a, s) => a + s.value, 0) || 1;
const r = 70, cx = 90, cy = 90, stroke = 28;
const r = 70, cx = 90, cy = 90, stroke = 22;
let cum = 0;
return segments.value.map(s => {
const pct = s.value / total;
@@ -34,13 +34,13 @@ const donut = computed(() => {
</script>
<template>
<SectionWrapper :id="id" title="Профиль контента рынка" subtitle="Что и как публикуют конкуренты" num="04">
<div style="display:grid;grid-template-columns:200px 1fr;gap:28px;align-items:start;">
<SectionWrapper :id="id" title="Профиль контента" subtitle="Анализ типов публикаций и технических параметров" num="4">
<div class="content-grid">
<!-- Donut -->
<div style="display:flex;flex-direction:column;align-items:center;gap:12px;">
<div style="position:relative;">
<svg width="180" height="180" viewBox="0 0 180 180">
<!-- 1. DONUT CHART -->
<div class="chart-col">
<div class="donut-wrap">
<svg class="donut-svg" viewBox="0 0 180 180">
<g v-for="seg in donut" :key="seg.key">
<circle
:cx="seg.cx"
@@ -51,51 +51,245 @@ const donut = computed(() => {
:stroke-width="seg.stroke"
:stroke-dasharray="`${seg.dash} ${seg.gap}`"
:stroke-dashoffset="seg.offset * seg.r"
class="donut-segment"
/>
</g>
<text x="90" y="86" text-anchor="middle" fill="rgba(255,255,255,0.8)" font-family="Unbounded,sans-serif" font-size="11" font-weight="700">Контент</text>
<text x="90" y="100" text-anchor="middle" fill="rgba(255,255,255,0.5)" font-family="Onest,sans-serif" font-size="10">рынка</text>
</svg>
<div class="donut-center">
<div class="center-title">КОНТЕНТ</div>
<div class="center-sub">НИШИ</div>
</div>
<div style="display:flex;flex-direction:column;gap:8px;width:100%;">
<div v-for="seg in donut" :key="seg.key" style="display:flex;align-items:center;gap:8px;">
<div :style="{ width:'10px', height:'10px', borderRadius:'3px', background: seg.color, flexShrink:0 }"/>
<span style="font-size:11px;color:rgba(255,255,255,0.7);flex:1;">{{ seg.label }}</span>
<span style="font-size:12px;font-weight:700;" :style="{ color: seg.color }">{{ seg.pct }}%</span>
</div>
<div class="legend-list">
<div v-for="seg in donut" :key="seg.key" class="legend-item">
<div class="legend-dot" :style="{ background: seg.color }"></div>
<span class="legend-label">{{ seg.label }}</span>
<span class="legend-pct" :style="{ color: seg.color }">{{ seg.pct }}%</span>
</div>
</div>
</div>
<!-- Meta-metrics -->
<div style="display:flex;flex-direction:column;gap:16px;">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);">Ключевые показатели контента</div>
<!-- 2. META METRICS -->
<div class="metrics-col">
<h4 class="metrics-title">Технические метрики</h4>
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;">
<div class="cards-row">
<div v-for="card in [
{ label:'Доля видео', value: fmtPct(data?.videoShare), color:'#2D7BFF', icon:'🎬', tip: 'Процент видео-контента (Reels, TikTok, Shorts) от общего числа постов' },
{ label:'CTA в постах', value: fmtPct(data?.ctaUsagePercent), color:'#00D48B', icon:'📣', tip: 'Процент постов, содержащих призыв к действию (CTA)' },
{ label:'Длина текста', value: fmtNum(data?.avgTextLengthCharacters) + ' симв.', color:'#A855F7', icon:'📝', small:true, tip: 'Среднее количество символов в тексте публикаций' },
]" :key="card.label" style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:12px;padding:16px;text-align:center;">
<div style="font-size:20px;margin-bottom:8px;">{{ card.icon }}</div>
<div style="font-family:Unbounded,sans-serif;font-size:20px;font-weight:900;line-height:1;margin-bottom:4px;" :style="{ color: card.color }">{{ card.value }}</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:rgba(255,255,255,0.4);">
{ label:'Доля видео', value: fmtPct(data?.videoShare), color:'var(--kai-blue)', icon:'pi-video', tip: 'Процент Reels, TikTok и Shorts' },
{ label:'CTA в постах', value: fmtPct(data?.ctaUsagePercent), color:'var(--kai-green)', icon:'pi-megaphone', tip: 'Процент призывов к действию' },
{ label:'Длина текста', value: fmtNum(data?.avgTextLengthCharacters) + ' симв.', color:'var(--kai-purple)', icon:'pi-pencil', tip: 'Среднее количество символов' },
]" :key="card.label" class="metric-card">
<div class="metric-card-icon" :style="{ color: card.color, background: card.color + '15' }">
<i class="pi" :class="card.icon"></i>
</div>
<div class="metric-card-val" :style="{ color: card.color }">{{ card.value }}</div>
<div class="metric-card-label">
{{ card.label }}
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="card.tip"></i>
</div>
</div>
</div>
<!-- Detailed bars -->
<div style="display:flex;flex-direction:column;gap:10px;">
<div v-for="seg in segments" :key="seg.key" style="display:flex;align-items:center;gap:12px;">
<span style="font-size:12px;font-weight:600;color:rgba(255,255,255,0.7);width:110px;flex-shrink:0;">{{ seg.label }}</span>
<div class="kai-progress-track" style="flex:1;height:8px;">
<!-- Progress bars -->
<div class="progress-list">
<div v-for="seg in segments" :key="seg.key" class="progress-item">
<span class="progress-label">{{ seg.label }}</span>
<div class="kai-progress-track">
<div class="kai-progress-fill" :style="{ width: seg.value + '%', background: seg.color }" />
</div>
<span style="font-size:12px;font-weight:700;width:42px;text-align:right;" :style="{ color: seg.color }">{{ fmtPct(seg.value) }}</span>
<span class="progress-pct" :style="{ color: seg.color }">{{ fmtPct(seg.value) }}</span>
</div>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.content-grid {
display: grid;
grid-template-columns: 200px 1fr;
gap: 40px;
align-items: start;
}
.chart-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 24px;
}
.donut-wrap {
position: relative;
width: 180px;
height: 180px;
}
.donut-segment {
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
}
.donut-center {
position: absolute;
inset: 0;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.center-title {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
color: var(--kai-txt);
letter-spacing: 0.1em;
}
.center-sub {
font-size: 9px;
font-weight: 700;
color: var(--kai-txt3);
opacity: 0.6;
}
.legend-list {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
}
.legend-item {
display: flex;
align-items: center;
gap: 10px;
}
.legend-dot {
width: 8px;
height: 8px;
border-radius: 3px;
}
.legend-label {
font-size: 11px;
color: var(--kai-txt2);
flex: 1;
}
.legend-pct {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
}
/* Metrics */
.metrics-col {
display: flex;
flex-direction: column;
gap: 24px;
}
.metrics-title {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.1em;
}
.cards-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.metric-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 16px;
padding: 20px;
text-align: center;
transition: all 0.3s;
}
.metric-card:hover {
border-color: var(--kai-border-hover);
background: var(--kai-card-hover);
transform: translateY(-2px);
}
.metric-card-icon {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
}
.metric-card-icon i {
font-size: 14px;
}
.metric-card-val {
font-family: 'Unbounded', sans-serif;
font-size: 16px;
font-weight: 800;
margin-bottom: 6px;
}
.metric-card-label {
font-size: 9.5px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.05em;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.progress-list {
display: flex;
flex-direction: column;
gap: 14px;
padding: 20px;
background: rgba(0,0,0,0.15);
border-radius: 16px;
border: 1px solid var(--kai-border);
}
.progress-item {
display: grid;
grid-template-columns: 110px 1fr 50px;
align-items: center;
gap: 16px;
}
.progress-label {
font-size: 11px;
font-weight: 700;
color: var(--kai-txt2);
}
.progress-pct {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-align: right;
}
@media (max-width: 768px) {
.content-grid {
grid-template-columns: 1fr;
gap: 32px;
}
}
</style>
@@ -12,17 +12,18 @@ const demandTrend = computed(() => {
const last = seasonality.value[seasonality.value.length - 1].value;
return first ? Math.round(((last - first) / first) * 100) : 0;
});
const trendDir = computed(() => demandTrend.value >= 0 ? '▲ +' + demandTrend.value + '%' : '▼ ' + demandTrend.value + '%');
const trendColor = computed(() => demandTrend.value >= 0 ? '#00D48B' : '#EF4444');
const trendColor = computed(() => demandTrend.value >= 0 ? 'var(--kai-green)' : 'var(--kai-red)');
// Area chart (SVG)
const chartPath = computed(() => {
if (seasonality.value.length === 0) return '';
if (seasonality.value.length === 0) return { stroke: '', fill: '', pts: [] };
const pts = seasonality.value.map(s => s.value);
const max = Math.max(...pts, 100);
const min = Math.min(...pts, 0);
const max = Math.max(...pts, 10);
const min = 0;
const range = max - min || 1;
const w = 400, h = 180;
const w = 500, h = 180;
const step = w / (pts.length - 1 || 1);
const coords = pts.map((val, i) => {
@@ -42,83 +43,289 @@ const chartPath = computed(() => {
</script>
<template>
<SectionWrapper :id="id" title="Поисковый спрос" subtitle="Как часто ищут ваш продукт и когда пиковый сезон" num="08">
<SectionWrapper :id="id" title="Поисковый спрос" subtitle="Анализ динамики интереса и сезонных колебаний" num="8">
<!-- Top metrics -->
<div style="display:flex;flex-wrap:wrap;gap:12px;margin-bottom:20px;">
<div class="kai-query-card" style="display:flex;align-items:center;gap:12px;background:rgba(45,123,255,0.08);border-color:rgba(45,123,255,0.3);">
<span style="font-size:20px;">📊</span>
<div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:rgba(45,123,255,0.8);">
Запросов/мес
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Среднее количество поисковых запросов в месяц (из Яндекс.Вордстат / Google Keyword Planner)'"></i>
<!-- TOP METRICS -->
<div class="demand-header-grid">
<div class="demand-metric-card primary">
<div class="d-card-icon">
<i class="pi pi-search"></i>
</div>
<div style="font-family:Unbounded,sans-serif;font-size:16px;font-weight:900;color:#2D7BFF;">{{ fmtNum(data?.avgMonthlySearches) }}</div>
<div class="d-card-info">
<div class="d-card-label">
Запросов / мес
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Среднее количество поисковых запросов в месяц'"></i>
</div>
<div class="d-card-val">{{ fmtNum(data?.avgMonthlySearches) }}</div>
</div>
</div>
<div class="kai-query-card" style="display:flex;align-items:center;gap:12px;">
<span style="font-size:20px;">📈</span>
<div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:rgba(255,255,255,0.4);">Тренд</div>
<div style="font-family:Unbounded,sans-serif;font-size:16px;font-weight:900;" :style="{ color: trendColor }">{{ trendDir }}</div>
<div class="demand-metric-card">
<div class="d-card-info">
<div class="d-card-label">Тренд</div>
<div class="d-card-val" :style="{ color: trendColor }">{{ trendDir }}</div>
</div>
</div>
<div class="kai-query-card" style="display:flex;align-items:center;gap:12px;" v-if="data?.peakPeriods?.length">
<span style="font-size:20px;">📅</span>
<div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;color:rgba(255,255,255,0.4);">Пик сезона</div>
<div style="font-family:Unbounded,sans-serif;font-size:14px;font-weight:700;color:rgba(255,255,255,0.9);">{{ data.peakPeriods[0] }}</div>
<div class="demand-metric-card" v-if="data?.peakPeriods?.length">
<div class="d-card-info">
<div class="d-card-label">Пик сезона</div>
<div class="d-card-val text-white opacity-90">{{ data.peakPeriods[0] }}</div>
</div>
</div>
</div>
<!-- Chart -->
<div class="kai-area-chart-wrap" style="margin-bottom:24px;">
<svg v-if="seasonality.length" viewBox="0 0 400 200" style="width:100%;height:220px;overflow:visible;">
<!-- CHART AREA -->
<div class="demand-chart-section">
<div class="chart-container">
<svg v-if="seasonality.length" viewBox="0 0 500 200" class="demand-svg">
<defs>
<linearGradient id="areaG" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#2D7BFF" stop-opacity="0.3"/>
<stop offset="100%" stop-color="#2D7BFF" stop-opacity="0"/>
<linearGradient id="demandGradient" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--kai-blue)" stop-opacity="0.2"/>
<stop offset="100%" stop-color="var(--kai-blue)" stop-opacity="0"/>
</linearGradient>
</defs>
<!-- Grid -->
<g stroke="rgba(255,255,255,0.05)" stroke-width="1">
<line x1="0" y1="45" x2="400" y2="45" stroke-dasharray="4 4"/>
<line x1="0" y1="90" x2="400" y2="90" stroke-dasharray="4 4"/>
<line x1="0" y1="135" x2="400" y2="135" stroke-dasharray="4 4"/>
<g class="chart-grid-lines">
<line x1="0" y1="45" x2="500" y2="45" />
<line x1="0" y1="90" x2="500" y2="90" />
<line x1="0" y1="135" x2="500" y2="135" />
</g>
<!-- Line and Area -->
<path :d="chartPath.fill" fill="url(#areaG)" />
<path :d="chartPath.stroke" fill="none" stroke="#2D7BFF" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" />
<!-- Data dots & X labels -->
<!-- Area & Line -->
<path :d="chartPath.fill" fill="url(#demandGradient)" />
<path :d="chartPath.stroke" fill="none" stroke="var(--kai-blue)" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" />
<!-- Points and Labels -->
<g v-for="(s, i) in seasonality" :key="i">
<circle :cx="chartPath.pts[i].x" :cy="chartPath.pts[i].y" r="4" fill="#2D7BFF" stroke="#0A0E1A" stroke-width="2"/>
<text :x="chartPath.pts[i].x" y="195" text-anchor="middle" fill="rgba(255,255,255,0.5)" font-size="10" font-family="Onest,sans-serif">{{ s.period.substring(0,3) }}</text>
<circle :cx="chartPath.pts[i].x" :cy="chartPath.pts[i].y" r="3.5" class="chart-dot" />
<text :x="chartPath.pts[i].x" y="195" text-anchor="middle" class="chart-axis-text">{{ s.period.substring(0,3) }}</text>
</g>
</svg>
<div v-else style="color:rgba(255,255,255,0.3);text-align:center;padding:40px;">Нет данных по сезонности</div>
<div v-else class="empty-chart">Данные по сезонности отсутствуют</div>
</div>
<!-- Peak cards below chart -->
<div v-if="data?.peakPeriods?.length" style="display:flex;flex-wrap:wrap;gap:12px;margin-top:20px;">
<div v-for="p in data.peakPeriods" :key="p" class="kai-peak-card">
<div class="kai-peak-title">🔥 {{ p }}</div>
<div class="kai-peak-sub">Готовиться за 46 недель</div>
<!-- Peak Recommendations -->
<div class="peak-box" v-if="data?.peakPeriods?.length">
<h5 class="peak-box-title">Рекомендации по сезонам</h5>
<div class="peak-tags">
<div v-for="p in data.peakPeriods" :key="p" class="peak-tag">
<div class="tag-top">🔥 {{ p }}</div>
<div class="tag-bottom">Разогрев за 4 недели</div>
</div>
</div>
</div>
</div>
<!-- Related Queries -->
<div v-if="data?.relatedQueries?.length">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:12px;">Связанные запросы</div>
<div style="display:grid;grid-template-columns:repeat(auto-fill, minmax(240px, 1fr));gap:12px;">
<div v-for="(q, i) in data.relatedQueries" :key="q" class="kai-query-card">
<div class="kai-query-label">🔍 "{{ q }}"</div>
<div style="display:flex;align-items:center;gap:8px;">
<div class="kai-progress-track" style="flex:1;height:4px;"><div class="kai-progress-fill" :style="{ width: Math.max(10, 100 - i*15)+'%' }" /></div>
<span style="font-size:10px;color:rgba(255,255,255,0.4);">Популярно</span>
<!-- RELATED QUERIES -->
<div class="related-queries-section" v-if="data?.relatedQueries?.length">
<h4 class="section-subtitle">Связанные запросы</h4>
<div class="queries-grid">
<div v-for="(q, i) in data.relatedQueries" :key="q" class="query-card">
<div class="query-text">
<i class="pi pi-search mr-2 opacity-40"></i>
{{ q }}
</div>
<div class="query-popularity">
<div class="kai-progress-track">
<div class="kai-progress-fill" :style="{ width: Math.max(15, 100 - i*12) + '%' }" />
</div>
<span class="pop-label">High Interest</span>
</div>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.demand-header-grid {
display: flex;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 24px;
}
.demand-metric-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 16px;
padding: 16px 20px;
display: flex;
align-items: center;
gap: 16px;
flex: 1;
min-width: 180px;
}
.demand-metric-card.primary {
background: rgba(var(--kai-blue-rgb), 0.05);
border-color: rgba(var(--kai-blue-rgb), 0.2);
}
.d-card-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: rgba(var(--kai-blue-rgb), 0.1);
color: var(--kai-blue);
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
}
.d-card-info {
display: flex;
flex-direction: column;
}
.d-card-label {
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.1em;
margin-bottom: 2px;
display: flex;
align-items: center;
gap: 4px;
}
.d-card-val {
font-family: 'Unbounded', sans-serif;
font-size: 14px;
font-weight: 800;
color: var(--kai-txt);
}
/* Chart */
.demand-chart-section {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 20px;
padding: 24px;
margin-bottom: 32px;
}
.chart-container {
height: 220px;
margin-bottom: 24px;
}
.demand-svg {
width: 100%;
height: 100%;
overflow: visible;
}
.chart-grid-lines {
stroke: var(--kai-border);
stroke-width: 1;
stroke-dasharray: 4 4;
}
.chart-dot {
fill: var(--kai-blue);
stroke: var(--kai-bg);
stroke-width: 2;
}
.chart-axis-text {
font-size: 9px;
font-weight: 700;
fill: var(--kai-txt3);
text-transform: uppercase;
}
.empty-chart {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: var(--kai-txt3);
font-size: 11px;
}
/* Peak recommendations */
.peak-box-title {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
margin-bottom: 16px;
}
.peak-tags {
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.peak-tag {
background: rgba(var(--kai-orange-rgb), 0.05);
border: 1px solid rgba(var(--kai-orange-rgb), 0.15);
padding: 10px 16px;
border-radius: 12px;
}
.tag-top {
font-size: 11.5px;
font-weight: 800;
color: var(--kai-orange);
margin-bottom: 2px;
}
.tag-bottom {
font-size: 9px;
font-weight: 600;
color: var(--kai-txt3);
opacity: 0.6;
}
/* Related Queries */
.section-subtitle {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
margin-bottom: 16px;
}
.queries-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
gap: 12px;
}
.query-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
padding: 14px 16px;
border-radius: 12px;
}
.query-text {
font-size: 12px;
font-weight: 700;
color: var(--kai-txt);
margin-bottom: 12px;
}
.query-popularity {
display: flex;
align-items: center;
gap: 12px;
}
.pop-label {
font-size: 9px;
font-weight: 700;
color: var(--kai-txt3);
white-space: nowrap;
}
</style>
@@ -11,7 +11,7 @@ const lvlClass = (lvl) => {
if (l.includes('низк') || l.includes('low')) return 'kai-level-low';
return 'kai-level-high';
};
const erColor = (v) => parseFloat(v) >= 4 ? '#00D48B' : parseFloat(v) >= 2 ? '#F59E0B' : '#EF4444';
const erColor = (v) => parseFloat(v) >= 4 ? 'var(--kai-green)' : parseFloat(v) >= 2 ? 'var(--kai-yellow)' : 'var(--kai-red)';
const stars = (r) => {
const n = parseFloat(r) || 0;
return '★'.repeat(Math.round(n)) + '☆'.repeat(5 - Math.round(n));
@@ -21,46 +21,111 @@ const fmtPct = (n) => n != null ? parseFloat(n).toFixed(1) + '%' : '—';
const kpis = computed(() => {
const d = props.data || {};
return [
{ icon: '👥', num: d.activeCompetitors ?? '—', label: 'активных конкурентов', sub: d.competitionLevel, subClass: lvlClass(d.competitionLevel), color: '#2D7BFF' },
{ icon: '📈', num: fmtPct(d.averageNicheEr), label: 'средний ER в нише', sub: 'Уровень вовлечённости аудитории', numColor: erColor(d.averageNicheEr), color: '#00D48B', term: 'er' },
{ icon: '', num: (parseFloat(d.averageRating) || 0).toFixed(1), label: 'средний рейтинг на картах', sub: stars(d.averageRating), color: '#F59E0B', term: 'rating' },
{ icon: '📊', num: d.demandTrend || '—', label: 'тренд спроса', sub: 'Изменение за период', color: '#A855F7', small: true, term: 'trend' },
{ icon: '🎯', num: d.businessStage || '—', label: 'этап бизнеса', sub: d.geography || '', color: '#FF7A2D', small: true },
{ icon: '', num: null, label: 'главный инсайт', sub: (d.keyFigures || [])[0] || '—', special: true, color: '#2D7BFF' },
{ icon: 'pi-users', num: d.activeCompetitors ?? '—', label: 'конкурентов', sub: d.competitionLevel, subClass: lvlClass(d.competitionLevel), color: 'var(--kai-blue)' },
{ icon: 'pi-chart-line', num: fmtPct(d.averageNicheEr), label: 'средний ER', sub: 'Вовлечённость', numColor: erColor(d.averageNicheEr), color: 'var(--kai-green)', term: 'er' },
{ icon: 'pi-star-fill', num: (parseFloat(d.averageRating) || 0).toFixed(1), label: 'рейтинг', sub: stars(d.averageRating), color: 'var(--kai-yellow)', term: 'rating' },
{ icon: 'pi-sort-alt', num: d.demandTrend || '—', label: 'тренд', sub: 'За период', color: 'var(--kai-purple)', term: 'trend' },
{ icon: 'pi-map-marker', num: d.businessStage || '—', label: 'этап', sub: d.geography || '', color: 'var(--kai-orange)' },
{ icon: 'pi-bolt', num: null, label: 'инсайт', sub: (d.keyFigures || [])[0] || '—', special: true, color: 'var(--kai-blue)' },
];
});
</script>
<template>
<SectionWrapper :id="id" title="Краткий обзор рынка" subtitle="Ключевые показатели ниши — всё самое важное на одном экране" num="00">
<SectionWrapper :id="id" title="Обзор рынка" subtitle="Ключевые показатели ниши и статус конкурентной среды" num="0">
<div class="kai-kpi-grid">
<div v-for="(k, i) in kpis" :key="i" class="kai-kpi" :style="k.special ? 'border-color:rgba(45,123,255,0.4);background:rgba(45,123,255,0.06);' : ''">
<div class="kai-kpi-icon" :style="{ background: k.color + '18' }">
<span>{{ k.icon }}</span>
<div v-for="(k, i) in kpis" :key="i" class="kai-kpi" :class="{ 'kai-kpi--special': k.special }">
<div class="kai-kpi-icon-wrap" :style="{ color: k.color, background: k.color + '10' }">
<i class="pi" :class="k.icon"></i>
</div>
<div v-if="!k.special">
<div class="kai-kpi-num" :style="k.numColor ? { color: k.numColor } : {}" :class="k.small ? 'text-2xl' : ''">{{ k.num }}</div>
<div v-if="!k.special" class="kai-kpi-content">
<div class="kai-kpi-num" :style="k.numColor ? { color: k.numColor } : {}">{{ k.num }}</div>
<div class="kai-kpi-label">
{{ k.label }}
<i v-if="k.term === 'er'" class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Engagement Rate: Процент аудителей, который активно взаимодействует с контентом (лайки, комментарии, репосты)'"></i>
<i v-if="k.term === 'rating'" class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Средний рейтинг конкурентов в таких сервисах, как 2ГИС, Яндекс.Карты и Google Maps'"></i>
<i v-if="k.term === 'trend'" class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Показывает общую динамику спроса в вашей нише: растет, падает или стабилен'"></i>
<i v-if="k.term" class="pi pi-info-circle kai-term-icon" v-tooltip.top="k.term === 'er' ? 'Процент аудитории, активно взаимодействующий с контентом' : k.term === 'rating' ? 'Средний рейтинг в 2ГИС, Яндекс и Google' : 'Динамика поискового спроса'"></i>
</div>
<div v-if="k.subClass" class="kai-level-badge" :class="k.subClass" style="font-size:11px;padding:3px 10px;">{{ k.sub }}</div>
<div v-if="k.subClass" class="kai-level-badge" :class="k.subClass">{{ k.sub }}</div>
<div v-else class="kai-kpi-sub">{{ k.sub }}</div>
</div>
<div v-else>
<div class="kai-kpi-label" style="color:#2D7BFF;text-transform:uppercase;letter-spacing:.08em;"> Главный инсайт</div>
<div class="kai-kpi-sub" style="color:rgba(255,255,255,0.8);font-size:13px;line-height:1.6;">{{ k.sub }}</div>
<div v-else class="kai-kpi-content">
<div class="kai-kpi-label-special"> Главный инсайт</div>
<div class="kai-kpi-sub-special">{{ k.sub }}</div>
</div>
</div>
</div>
<!-- Key insights strip -->
<div v-if="(data?.keyFigures || []).length" class="kai-insight-strip" style="margin-top:20px;">
<div v-if="(data?.keyFigures || []).length" class="kai-insight-strip">
<div v-for="(f, i) in (data?.keyFigures || []).slice(0,4)" :key="i" class="kai-insight-tag">
<span style="opacity:.5;margin-right:4px;">{{ ['📊','⭐','📈','🎯'][i] }}</span> {{ f }}
<div class="kai-insight-tag-dot" :style="{ background: ['var(--kai-blue)','var(--kai-yellow)','var(--kai-green)','var(--kai-purple)'][i] }"></div>
{{ f }}
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.kai-kpi-icon-wrap {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 12px;
}
.kai-kpi-icon-wrap i {
font-size: 14px;
}
.kai-kpi--special {
border-color: rgba(var(--kai-blue-rgb), 0.3) !important;
background: rgba(var(--kai-blue-rgb), 0.05) !important;
}
.kai-kpi-label-special {
color: var(--kai-blue);
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
margin-bottom: 4px;
}
.kai-kpi-sub-special {
font-size: 11.5px;
color: var(--kai-txt);
line-height: 1.5;
font-weight: 500;
}
.kai-insight-strip {
margin-top: 24px;
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.kai-insight-tag {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 14px;
background: var(--kai-card);
border: 1px solid var(--kai-border);
border-radius: 10px;
font-size: 11px;
color: var(--kai-txt2);
font-weight: 500;
}
.kai-insight-tag-dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
</style>
@@ -4,27 +4,30 @@ import { computed } from 'vue';
const props = defineProps({ id: String, data: Object });
const GAUGE_COLORS = ['#00D48B','#F59E0B','#FF7A2D','#EF4444'];
const gaugeColor = (v) => {
const n = parseFloat(v) || 0;
if (n <= 40) return GAUGE_COLORS[0];
if (n <= 70) return GAUGE_COLORS[1];
if (n <= 85) return GAUGE_COLORS[2];
return GAUGE_COLORS[3];
if (n <= 40) return 'var(--kai-green)';
if (n <= 70) return 'var(--kai-yellow)';
if (n <= 85) return 'var(--kai-orange)';
return 'var(--kai-red)';
};
const gaugeLabel = (v) => {
const n = parseFloat(v) || 0;
if (n <= 30) return '🟢 Свободный рынок';
if (n <= 60) return '🟡 Умеренная конкуренция';
if (n <= 85) return '🟠 Высокая конкуренция';
return '🔴 Перегретый рынок';
if (n <= 30) return 'Свободный рынок';
if (n <= 60) return 'Умеренная конкуренция';
if (n <= 85) return 'Высокая конкуренция';
return 'Перегретый рынок';
};
const cityStatus = (row) => {
if ((row.activePlayers > 30 && row.avgEr > 4) || row.activePlayers > 25) return { text: 'Высококонкурентный', color: '#EF4444' };
if (row.activePlayers > 15) return { text: 'Конкурентный', color: '#FF7A2D' };
return { text: 'Свободный', color: '#00D48B' };
if ((row.activePlayers > 30 && row.avgEr > 4) || row.activePlayers > 25) return { text: 'HIGH', color: 'var(--kai-red)' };
if (row.activePlayers > 15) return { text: 'MODERATE', color: 'var(--kai-orange)' };
return { text: 'OPEN', color: 'var(--kai-green)' };
};
const fmtPct = (n) => n != null ? parseFloat(n).toFixed(2) + '%' : '—';
const cities = computed(() => {
const arr = props.data?.cityComparison || [];
return arr.map(c => ({
@@ -35,9 +38,10 @@ const cities = computed(() => {
avgPostsPerMonth: parseInt(c.averagePostsPerMonth ?? c.avgPostsPerMonth) || 0
}));
});
const density = computed(() => parseFloat(props.data?.densityIndex) || 0);
// SVG gauge arc calculation
// SVG gauge arc
const arcPath = computed(() => {
const r = 80, cx = 100, cy = 100;
const startAngle = -180, sweepAngle = 180 * (density.value / 100);
@@ -47,68 +51,201 @@ const arcPath = computed(() => {
const x2 = cx + r * Math.cos(toRad(startAngle + sweepAngle));
const y2 = cy + r * Math.sin(toRad(startAngle + sweepAngle));
const lg = sweepAngle > 180 ? 1 : 0;
return `M ${x1} ${y1} A ${r} ${r} 0 ${lg} 1 ${x2} ${y2}`;
return `M 20 100 A ${r} ${r} 0 ${lg} 1 ${x2} ${y2}`;
});
const sortKey = { key: 'activePlayers', dir: -1 };
const sortedCities = computed(() => [...cities.value].sort((a,b) => (b[sortKey.key] - a[sortKey.key]) * sortKey.dir));
const sortedCities = computed(() => [...cities.value].sort((a,b) => b.activePlayers - a.activePlayers));
</script>
<template>
<SectionWrapper :id="id" title="Конкуренция по городам" subtitle="Детальное сравнение рынков в каждом городе" num="02">
<div style="display:grid;grid-template-columns:200px 1fr;gap:28px;align-items:start;">
<SectionWrapper :id="id" title="Гео-структура" subtitle="Плотность конкуренции и активность по городам" num="2">
<div class="geo-container">
<!-- Gauge -->
<div class="kai-gauge-wrap">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:12px;text-align:center;">
Индекс плотности
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Показывает, насколько сложно зайти на рынок в этом городе. Выше 70 рынок перегрет, нужны большие бюджеты.'"></i>
</div>
<svg width="200" height="120" viewBox="0 0 200 120">
<!-- GAUGE SIDE -->
<div class="gauge-col">
<div class="gauge-label">Density Index</div>
<div class="gauge-svg-wrap">
<svg class="gauge-svg" viewBox="0 0 200 120">
<!-- Track -->
<path d="M 20 100 A 80 80 0 0 1 180 100" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="14" stroke-linecap="round"/>
<path d="M 20 100 A 80 80 0 0 1 180 100" class="gauge-track"/>
<!-- Fill -->
<path :d="arcPath.replace('M 20 100', 'M 20 100')" fill="none" :stroke="gaugeColor(density)" stroke-width="14" stroke-linecap="round">
<animateTransform attributeName="transform" type="rotate" from="0 100 100" to="0 100 100" dur="1s"/>
</path>
<!-- Value -->
<text x="100" y="96" text-anchor="middle" :fill="gaugeColor(density)" font-family="Unbounded,sans-serif" font-size="28" font-weight="900">{{ Math.round(density) }}</text>
<text x="100" y="112" text-anchor="middle" fill="rgba(255,255,255,0.4)" font-family="Onest,sans-serif" font-size="10">из 100</text>
<path :d="arcPath" :stroke="gaugeColor(density)" class="gauge-fill" />
<text x="100" y="94" text-anchor="middle" class="gauge-val" :fill="gaugeColor(density)">{{ Math.round(density) }}</text>
<text x="100" y="110" text-anchor="middle" class="gauge-max">OF 100</text>
</svg>
<div style="font-size:11px;font-weight:700;text-align:center;padding:4px 12px;border-radius:999px;background:rgba(255,255,255,0.05);color:rgba(255,255,255,0.7);">{{ gaugeLabel(density) }}</div>
</div>
<div class="gauge-status-badge" :style="{ background: gaugeColor(density) + '15', color: gaugeColor(density) }">
{{ gaugeLabel(density) }}
</div>
</div>
<!-- Table -->
<div style="overflow-x:auto;">
<!-- TABLE SIDE -->
<div class="table-col">
<table class="kai-table">
<thead><tr>
<th>Город</th><th>Игроков</th>
<th>
Ср. ER
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Средняя вовлечённость аудитории'"></i>
</th>
<th>Рейтинг</th><th>Постов/мес</th><th>Статус</th>
</tr></thead>
<thead>
<tr>
<th>Город</th>
<th width="80">Игроков</th>
<th width="80">Ср. ER</th>
<th width="80">Rating</th>
<th width="100">Интенсивность</th>
<th width="100">Статус</th>
</tr>
</thead>
<tbody>
<tr v-for="c in sortedCities" :key="c.city">
<td><span style="font-weight:700;">🏙 {{ c.city }}</span></td>
<td><span style="font-family:Unbounded,sans-serif;font-weight:700;color:#2D7BFF;">{{ c.activePlayers }}</span></td>
<td><span :style="{ color: parseFloat(c.avgEr)>=4?'#00D48B':parseFloat(c.avgEr)>=2?'#F59E0B':'#EF4444', fontWeight:700 }">{{ fmtPct(c.avgEr) }}</span></td>
<td><span style="color:#F59E0B;font-weight:700;"> {{ (c.avgRating||0).toFixed(1) }}</span></td>
<td class="city-cell">
<i class="pi pi-map-marker text-[10px] text-surface-500 mr-2" />
{{ c.city }}
</td>
<td class="font-bold text-blue-500">{{ c.activePlayers }}</td>
<td class="font-bold" :style="{ color: parseFloat(c.avgEr)>=4?'var(--kai-green)':parseFloat(c.avgEr)>=2?'var(--kai-yellow)':'var(--kai-red)' }">{{ fmtPct(c.avgEr) }}</td>
<td class="font-bold text-yellow-500"> {{ (c.avgRating||0).toFixed(1) }}</td>
<td>
<div style="display:flex;align-items:center;gap:8px;">
<div style="width:60px;height:4px;background:rgba(255,255,255,0.06);border-radius:2px;">
<div :style="{ width:Math.min(c.avgPostsPerMonth/30*100,100)+'%', height:'100%', background:'#2D7BFF', borderRadius:'2px' }"/>
</div>
<span style="font-weight:600;font-size:12px;">{{ c.avgPostsPerMonth }}</span>
<div class="intensity-bar-wrap">
<div class="intensity-bar" :style="{ width: Math.min(c.avgPostsPerMonth/25*100, 100) + '%' }" />
<span class="intensity-val">{{ c.avgPostsPerMonth }}</span>
</div>
</td>
<td><span style="padding:3px 10px;border-radius:999px;font-size:10px;font-weight:700;" :style="{ background: cityStatus(c).color+'18', color: cityStatus(c).color }">{{ cityStatus(c).text }}</span></td>
<td>
<span class="status-pill" :style="{ color: cityStatus(c).color, background: cityStatus(c).color + '10', borderColor: cityStatus(c).color + '20' }">
{{ cityStatus(c).text }}
</span>
</td>
</tr>
<tr v-if="!sortedCities.length"><td colspan="6" style="text-align:center;color:rgba(255,255,255,0.3);padding:24px;">Нет данных</td></tr>
<tr v-if="!sortedCities.length"><td colspan="6" class="empty-table">Нет данных</td></tr>
</tbody>
</table>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.geo-container {
display: grid;
grid-template-columns: 200px 1fr;
gap: 40px;
align-items: start;
}
.gauge-col {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.gauge-label {
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.1em;
}
.gauge-svg-wrap {
position: relative;
width: 200px;
}
.gauge-track {
fill: none;
stroke: var(--kai-border);
stroke-width: 14;
stroke-linecap: round;
}
.gauge-fill {
fill: none;
stroke-width: 14;
stroke-linecap: round;
transition: all 1s ease-out;
}
.gauge-val {
font-family: 'Unbounded', sans-serif;
font-size: 28px;
font-weight: 800;
}
.gauge-max {
font-size: 8px;
font-weight: 700;
fill: var(--kai-txt3);
letter-spacing: 0.1em;
}
.gauge-status-badge {
font-size: 9.5px;
font-weight: 800;
padding: 6px 16px;
border-radius: 99px;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.city-cell {
font-weight: 700;
font-size: 11.5px;
color: var(--kai-txt);
}
.intensity-bar-wrap {
display: flex;
align-items: center;
gap: 10px;
}
.intensity-bar {
flex: 1;
height: 4px;
background: var(--kai-border);
border-radius: 2px;
position: relative;
overflow: hidden;
}
.intensity-bar::after {
content: '';
position: absolute;
inset: 0;
background: var(--kai-blue);
width: inherit;
border-radius: inherit;
}
.intensity-val {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
width: 20px;
text-align: right;
}
.status-pill {
padding: 3px 10px;
border-radius: 6px;
font-size: 8px;
font-weight: 900;
border: 1px solid transparent;
display: inline-block;
letter-spacing: 0.05em;
}
.empty-table {
text-align: center;
color: var(--kai-txt3);
padding: 24px;
font-size: 11px;
}
@media (max-width: 768px) {
.geo-container {
grid-template-columns: 1fr;
gap: 32px;
}
}
</style>
@@ -1,26 +1,81 @@
<script setup>
import { computed } from 'vue';
defineProps({ status: String, exec: Object });
const props = defineProps({ status: String, exec: Object });
const emit = defineEmits(['strategy', 'back']);
const statusOk = computed(() => ({ COMPLETED: true })[props?.status]);
</script>
<script>
export default { name: 'V4Header' };
</script>
<template>
<header class="kai-header">
<div class="flex items-center gap-3">
<div class="kai-logo">KAI</div>
<div class="kai-title-area">
<div class="kai-title-niche">Анализ рынка · {{ exec?.geography || '...' }}</div>
<div class="kai-title-sub">{{ exec?.businessStage || '' }} · KONTURAI ANALYTICS v4.0</div>
<div class="kai-title-niche">Market Intelligence · {{ exec?.geography || '' }}</div>
<div class="kai-title-sub">Professional Ed. · KAI Prime (2026)</div>
</div>
<div style="display:flex;align-items:center;gap:10px;margin-left:auto;">
<span v-if="status === 'COMPLETED'" class="kai-badge-status green"> Анализ завершён</span>
<span v-else-if="status === 'PROCESSING' || status === 'QUEUED'" class="kai-badge-status yellow"> Обработка...</span>
<button class="kai-pdf-btn" @click="$emit('back')"> Список</button>
<button v-if="status === 'COMPLETED'" class="kai-cta-btn" style="padding:8px 18px;font-size:12px;margin:0;" @click="$emit('strategy')">
Создать Стратегию
</div>
<div class="flex items-center gap-3 ml-auto">
<div v-if="status === 'COMPLETED'" class="kai-badge-status green">
<span class="w-1 h-1 rounded-full bg-emerald-500 animate-pulse"></span>
Ready
</div>
<div v-else-if="status === 'PROCESSING' || status === 'QUEUED'" class="kai-badge-status yellow">
<span class="w-1 h-1 rounded-full bg-yellow-500 animate-pulse"></span>
Ingesting...
</div>
<button class="kai-btn-minor" @click="$emit('back')">
<i class="pi pi-list text-[10px]" />
</button>
<button v-if="status === 'COMPLETED'" class="kai-cta-btn-header" @click="$emit('strategy')">
Generate Strategy
<i class="pi pi-arrow-right ml-1 text-[9px] opacity-60" />
</button>
</div>
</header>
</template>
<style scoped>
.kai-btn-minor {
background: var(--kai-card);
border: 1px solid var(--kai-border);
color: var(--kai-txt3);
width: 28px;
height: 28px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
cursor: pointer;
}
.kai-btn-minor:hover {
background: var(--kai-card-hover);
border-color: var(--kai-border-hover);
color: var(--kai-txt);
}
.kai-cta-btn-header {
background: var(--kai-blue);
color: #fff;
border: none;
padding: 6px 14px;
border-radius: 8px;
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: all 0.3s;
cursor: pointer;
box-shadow: 0 4px 12px rgba(var(--kai-blue-rgb), 0.2);
}
.kai-cta-btn-header:hover {
transform: translateY(-1px);
filter: brightness(1.1);
box-shadow: 0 6px 16px rgba(var(--kai-blue-rgb), 0.3);
}
</style>
@@ -4,66 +4,229 @@ import { computed } from 'vue';
const props = defineProps({ id: String, data: Object, demand: Object });
const PLATFORM_COLORS = { Instagram:'#E1306C', TikTok:'#69C9D0', Telegram:'#2AABEE', '2GIS':'#00B140', YouTube:'#FF0000', GoogleMaps:'#4285F4' };
const PLATFORM_COLORS = {
Instagram: '#E1306C',
TikTok: '#69C9D0',
Telegram: '#2AABEE',
'2GIS': '#00B140',
YouTube: '#FF0000',
GoogleMaps: '#4285F4'
};
const platforms = computed(() => {
const pl = props.data?.activePlayersByPlatform || {};
const max = Math.max(...Object.values(pl), 1);
return Object.entries(pl).map(([k, v]) => ({ name: k, count: v, pct: Math.round(v / max * 100), color: PLATFORM_COLORS[k] || '#2D7BFF' })).sort((a,b)=>b.count-a.count);
return Object.entries(pl).map(([k, v]) => ({
name: k,
count: v,
pct: Math.round(v / max * 100),
color: PLATFORM_COLORS[k] || 'var(--kai-blue)'
})).sort((a,b) => b.count - a.count);
});
const cities = computed(() => {
const cd = props.data?.cityDistribution || {};
const max = Math.max(...Object.values(cd), 1);
return Object.entries(cd).map(([k, v]) => ({ city: k, count: v, pct: Math.round(v / max * 100) })).sort((a,b)=>b.count-a.count).slice(0, 7);
return Object.entries(cd).map(([k, v]) => ({
city: k,
count: v,
pct: Math.round(v / max * 100)
})).sort((a,b) => b.count - a.count).slice(0, 7);
});
</script>
<template>
<SectionWrapper :id="id" title="Ландшафт рынка" subtitle="Где и сколько конкурентов присутствуют" num="01">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;">
<!-- Platforms horizontal bar chart -->
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:16px;">Конкуренты по платформам</div>
<div style="display:flex;flex-direction:column;gap:12px;">
<div v-for="p in platforms" :key="p.name" style="display:flex;align-items:center;gap:10px;">
<div style="width:90px;font-size:12px;font-weight:600;color:rgba(255,255,255,0.7);text-align:right;flex-shrink:0;">{{ p.name }}</div>
<div style="flex:1;height:28px;background:rgba(255,255,255,0.04);border-radius:6px;overflow:hidden;position:relative;">
<div :style="{ width: p.pct+'%', height:'100%', background: p.color, borderRadius:'6px', transition:'width 1.2s ease-out', display:'flex', alignItems:'center', paddingLeft:'10px' }">
<span style="font-size:12px;font-weight:700;color:#fff;white-space:nowrap;">{{ p.count }}</span>
<SectionWrapper :id="id" title="Ландшафт рынка" subtitle="Распределение игроков по платформам и географии" num="1">
<div class="landscape-grid">
<!-- 1. PLATFORMS -->
<div class="landscape-card">
<h3 class="landscape-label">Конкуренты по платформам</h3>
<div class="platform-list">
<div v-for="p in platforms" :key="p.name" class="platform-item">
<div class="platform-name">{{ p.name }}</div>
<div class="platform-track">
<div class="platform-fill" :style="{ width: p.pct + '%', background: p.color }">
<span class="platform-count">{{ p.count }}</span>
</div>
</div>
</div>
<div v-if="!platforms.length" style="color:rgba(255,255,255,0.3);font-size:13px;text-align:center;padding:20px 0;">Нет данных</div>
<div v-if="!platforms.length" class="empty-state">Нет данных</div>
</div>
</div>
<!-- Cities table -->
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:16px;">Распределение по городам</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<div v-for="c in cities" :key="c.city" style="display:grid;grid-template-columns:100px 40px 1fr;align-items:center;gap:10px;">
<span style="font-size:12px;font-weight:600;color:rgba(255,255,255,0.8);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">{{ c.city }}</span>
<span style="font-size:13px;font-weight:700;color:#2D7BFF;text-align:right;">{{ c.count }}</span>
<!-- 2. CITIES -->
<div class="landscape-card">
<h3 class="landscape-label">Топ городов присутствия</h3>
<div class="city-list">
<div v-for="c in cities" :key="c.city" class="city-item">
<span class="city-name">{{ c.city }}</span>
<span class="city-count">{{ c.count }}</span>
<div class="kai-progress-track">
<div class="kai-progress-fill" :style="{ width: c.pct + '%' }" />
</div>
</div>
<div v-if="!cities.length" style="color:rgba(255,255,255,0.3);font-size:13px;text-align:center;padding:20px 0;">Нет данных</div>
<div v-if="!cities.length" class="empty-state">Нет данных</div>
</div>
<!-- Niche reputation -->
<div v-if="data?.nicheReputationLevel" style="margin-top:20px;padding:14px 18px;background:rgba(0,212,139,0.06);border:1px solid rgba(0,212,139,0.2);border-radius:12px;display:flex;align-items:center;justify-content:space-between;">
<div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(0,212,139,0.7);margin-bottom:4px;">
<!-- REPUTATION MINI-BANNER -->
<div v-if="data?.nicheReputationLevel" class="reputation-banner">
<div class="reputation-info">
<div class="reputation-label">
Репутация ниши
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Средний рейтинг среди всех конкурентов. Отражает общее доверие клиентов к данному рынку.'"></i>
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Средний рейтинг среди всех конкурентов.'"></i>
</div>
<div style="font-family:Unbounded,sans-serif;font-size:24px;font-weight:900;color:#00D48B;">{{ data.nicheReputationLevel }} <span style="font-size:14px;">/ 5.0</span></div>
<div class="reputation-value">
{{ data.nicheReputationLevel }} <span class="reputation-total">/ 5.0</span>
</div>
<div style="font-size:28px;">🏆</div>
</div>
<div class="reputation-icon">🏆</div>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.landscape-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 32px;
}
.landscape-label {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.12em;
color: var(--kai-txt3);
margin-bottom: 24px;
}
.platform-list, .city-list {
display: flex;
flex-direction: column;
gap: 14px;
}
.platform-item {
display: flex;
align-items: center;
gap: 12px;
}
.platform-name {
width: 90px;
font-size: 11px;
font-weight: 700;
color: var(--kai-txt2);
text-align: right;
flex-shrink: 0;
}
.platform-track {
flex: 1;
height: 24px;
background: var(--kai-card);
border-radius: 6px;
overflow: hidden;
position: relative;
border: 1px solid var(--kai-border);
}
.platform-fill {
height: 100%;
border-radius: 4px;
transition: width 1.2s cubic-bezier(0.4, 0, 0.2, 1);
display: flex;
align-items: center;
padding-left: 10px;
box-shadow: 0 0 15px rgba(255,255,255,0.05);
}
.platform-count {
font-size: 10px;
font-weight: 900;
color: #fff;
font-family: 'Unbounded', sans-serif;
}
.city-item {
display: grid;
grid-template-columns: 100px 30px 1fr;
align-items: center;
gap: 12px;
}
.city-name {
font-size: 11px;
font-weight: 700;
color: var(--kai-txt2);
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.city-count {
font-size: 11.5px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
color: var(--kai-blue);
text-align: right;
}
.reputation-banner {
margin-top: 32px;
padding: 16px 20px;
background: linear-gradient(135deg, rgba(var(--kai-green-rgb), 0.1) 0%, rgba(var(--kai-green-rgb), 0.03) 100%);
border: 1px solid rgba(var(--kai-green-rgb), 0.2);
border-radius: 12px;
display: flex;
align-items: center;
justify-content: space-between;
}
.reputation-label {
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--kai-green);
margin-bottom: 4px;
display: flex;
align-items: center;
gap: 4px;
}
.reputation-value {
font-family: 'Unbounded', sans-serif;
font-size: 24px;
font-weight: 900;
color: var(--kai-green);
line-height: 1;
}
.reputation-total {
font-size: 12px;
opacity: 0.5;
}
.reputation-icon {
font-size: 24px;
filter: drop-shadow(0 0 8px rgba(var(--kai-green-rgb), 0.4));
}
.empty-state {
color: var(--kai-txt3);
font-size: 11px;
text-align: center;
padding: 24px 0;
font-weight: 500;
}
@media (max-width: 768px) {
.landscape-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -8,11 +8,11 @@ const mode = computed(() => props.data?.mode || 'COMPARISON');
const isBench = computed(() => mode.value === 'BENCHMARK');
const axes = [
{ key: 'activity', label: '📊 Активность', tip: 'Количество и регулярность публикаций' },
{ key: 'engagement', label: '💬 Вовлечённость', tip: 'Количество лайков, комментариев, репостов' },
{ key: 'video', label: '📹 Видео', tip: 'Доля современного формата Reels / TikTok в контенте' },
{ key: 'reputation', label: 'Репутация', tip: 'Рейтинг на агрегаторах и картах (2ГИС, Яндекс)' },
{ key: 'frequency', label: '📅 Частота', tip: 'Сколько проходит времени между постами' }
{ key: 'activity', label: 'Активность', tip: 'Количество и регулярность публикаций' },
{ key: 'engagement', label: 'Вовлечённость', tip: 'Лайки, комментарии, репосты' },
{ key: 'video', label: 'Видео', tip: 'Доля Reels / TikTok' },
{ key: 'reputation', label: 'Репутация', tip: 'Рейтинг на агрегаторах' },
{ key: 'frequency', label: 'Частота', tip: 'Интервал между постами' }
];
const uMet = computed(() => props.data?.userMetrics || {});
@@ -22,18 +22,18 @@ const gapData = computed(() => axes.map(a => {
const m = parseFloat(mMet.value[a.key]) || 0;
const u = parseFloat(uMet.value[a.key]) || 0;
const diff = u - m;
let prio = 'NEED', prioText = '🟡 Нужно';
if (diff < -60) { prio = 'CRIT'; prioText = '🔴 Критично'; }
else if (diff < -40) { prio = 'IMP'; prioText = '🟠 Важно'; }
else if (diff >= 0) { prio = 'AHEAD'; prioText = '🟢 Вы впереди'; }
return { label: a.label, m, u, diff, prio, prioText };
let prio = 'NEED', prioText = 'НУЖНО';
if (diff < -60) { prio = 'CRIT'; prioText = 'КРИТИЧНО'; }
else if (diff < -40) { prio = 'IMP'; prioText = 'ВАЖНО'; }
else if (diff >= 0) { prio = 'AHEAD'; prioText = 'ЛИДЕР'; }
return { label: a.label, m, u, diff, prio, prioText, tip: a.tip };
}));
// Radar Chart Math
const radarPath = (metrics, scale=0.8) => {
const pts = axes.map((a, i) => {
const v = parseFloat(metrics[a.key]) || 0;
const r = (v / 100) * 100 * scale; // max radius 100
const v = Math.min(parseFloat(metrics[a.key]) || 0, 100);
const r = (v / 100) * 100 * scale;
const angle = (Math.PI * 2 * i) / axes.length - Math.PI / 2;
return `${150 + r * Math.cos(angle)},${150 + r * Math.sin(angle)}`;
});
@@ -45,73 +45,291 @@ const userPath = computed(() => isBench.value ? '' : radarPath(uMet.value));
</script>
<template>
<SectionWrapper :id="id" title="Ваша позиция на рынке" :subtitle="isBench ? 'Вы ещё не в соцсетях — вот как выглядит рынок, который вас ждёт' : 'Сравнение вашего аккаунта со средним по рынку'" num="09">
<div style="display:grid;grid-template-columns:300px 1fr;gap:32px;align-items:start;">
<SectionWrapper :id="id" title="Позиционирование" :subtitle="isBench ? 'Сравнение потенциала входа с текущими лидерами' : 'Ваш аккаунт в контексте средних показателей рынка'" num="9">
<div class="pos-grid">
<!-- Radar Chart -->
<div>
<div style="position:relative;width:300px;height:300px;background:rgba(255,255,255,0.02);border-radius:50%;border:1px solid rgba(255,255,255,0.05);">
<!-- Web -->
<svg width="300" height="300" viewBox="0 0 300 300">
<polygon points="150,50 245,119 209,230 91,230 54,119" fill="none" stroke="rgba(255,255,255,0.1)" stroke-width="1"/>
<polygon points="150,100 197,135 178,190 120,190 102,135" fill="none" stroke="rgba(255,255,255,0.05)" stroke-width="1"/>
<line x1="150" y1="150" x2="150" y2="50" stroke="rgba(255,255,255,0.1)" />
<line x1="150" y1="150" x2="245" y2="119" stroke="rgba(255,255,255,0.1)" />
<line x1="150" y1="150" x2="209" y2="230" stroke="rgba(255,255,255,0.1)" />
<line x1="150" y1="150" x2="91" y2="230" stroke="rgba(255,255,255,0.1)" />
<line x1="150" y1="150" x2="54" y2="119" stroke="rgba(255,255,255,0.1)" />
<!-- RADAR SECTION -->
<div class="radar-col">
<div class="radar-container">
<svg viewBox="0 0 300 300" class="radar-svg">
<!-- Web Circles -->
<circle cx="150" cy="150" r="100" class="radar-web" />
<circle cx="150" cy="150" r="75" class="radar-web" />
<circle cx="150" cy="150" r="50" class="radar-web" />
<circle cx="150" cy="150" r="25" class="radar-web" />
<!-- Market -->
<path :d="marketPath" fill="rgba(45,123,255,0.2)" stroke="#2D7BFF" stroke-width="2" />
<!-- User -->
<path v-if="!isBench" :d="userPath" fill="rgba(0,212,139,0.3)" stroke="#00D48B" stroke-width="3" />
<!-- Labels -->
<text x="150" y="35" text-anchor="middle" fill="rgba(255,255,255,0.6)" font-size="10" font-weight="700">Активность</text>
<text x="260" y="115" text-anchor="start" fill="rgba(255,255,255,0.6)" font-size="10" font-weight="700">Вовлечённость</text>
<text x="215" y="245" text-anchor="start" fill="rgba(255,255,255,0.6)" font-size="10" font-weight="700">Видео</text>
<text x="85" y="245" text-anchor="end" fill="rgba(255,255,255,0.6)" font-size="10" font-weight="700">Репутация</text>
<text x="40" y="115" text-anchor="end" fill="rgba(255,255,255,0.6)" font-size="10" font-weight="700">Частота</text>
<!-- Axis lines -->
<g v-for="(a, i) in axes" :key="i">
<line x1="150" y1="150"
:x2="150 + 100 * Math.cos((Math.PI * 2 * i) / axes.length - Math.PI / 2)"
:y2="150 + 100 * Math.sin((Math.PI * 2 * i) / axes.length - Math.PI / 2)"
class="radar-axis-line" />
<text :x="150 + 115 * Math.cos((Math.PI * 2 * i) / axes.length - Math.PI / 2)"
:y="150 + 115 * Math.sin((Math.PI * 2 * i) / axes.length - Math.PI / 2)"
text-anchor="middle" class="radar-axis-label">{{ a.label }}</text>
</g>
<text v-if="isBench" x="150" y="150" text-anchor="middle" fill="#2D7BFF" font-family="Unbounded,sans-serif" font-size="11" font-weight="700">СТАРТ</text>
<!-- Paths -->
<path :d="marketPath" class="radar-path market" />
<path v-if="!isBench" :d="userPath" class="radar-path user" />
<text v-if="isBench" x="150" y="155" text-anchor="middle" class="radar-bench-label">BENCHMARK</text>
</svg>
</div>
<div class="kai-radar-labels">
<div class="kai-radar-label"><div class="kai-radar-dot" style="background:#2D7BFF;" /> Средний рынок</div>
<div class="kai-radar-label" v-if="!isBench"><div class="kai-radar-dot" style="background:#00D48B;" /> Ваш аккаунт</div>
<div class="radar-legend">
<div class="legend-item market">
<span class="dot"></span> Средний рынок
</div>
<div v-if="!isBench" class="legend-item user">
<span class="dot"></span> Ваш аккаунт
</div>
</div>
</div>
<!-- Gap Table & Focus -->
<div>
<div style="font-size:14px;font-family:Unbounded,sans-serif;font-weight:700;color:rgba(255,255,255,0.9);margin-bottom:12px;">Где вы отстаёте от рынка</div>
<table class="kai-table kai-gap-table" style="margin-bottom:24px;">
<thead><tr><th>Параметр</th><th>Рынок</th><th v-if="!isBench">Вы</th><th>Разрыв</th><th>Приоритет</th></tr></thead>
<!-- GAP TABLE SECTION -->
<div class="table-col">
<h4 class="pos-subtitle">Анализ разрывов (Gap Analysis)</h4>
<div class="table-card">
<table class="kai-table">
<thead>
<tr>
<th>Параметр</th>
<th width="70">Рынок</th>
<th v-if="!isBench" width="70">Вы</th>
<th width="80">Разрыв</th>
<th width="100">Статус</th>
</tr>
</thead>
<tbody>
<tr v-for="g in gapData" :key="g.label">
<td style="font-weight:600;">
<td class="param-cell">
{{ g.label }}
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="g.tip"></i>
</td>
<td style="font-weight:700;color:#2D7BFF;">{{ Math.round(g.m) }}</td>
<td v-if="!isBench"><span style="font-weight:700;color:#00D48B;">{{ Math.round(g.u) }}</span></td>
<td style="font-family:Unbounded,sans-serif;font-weight:700;" :style="{ color: g.diff >= 0 ? '#00D48B' : (g.diff < -40 ? '#EF4444' : '#F59E0B') }">{{ g.diff >= 0 ? '+'+Math.round(g.diff) : Math.round(g.diff) }}</td>
<td class="font-bold text-blue-500">{{ Math.round(g.m) }}</td>
<td v-if="!isBench" class="font-bold text-green-500">{{ Math.round(g.u) }}</td>
<td class="gap-cell" :style="{ color: g.diff >= 0 ? 'var(--kai-green)' : (g.diff < -40 ? 'var(--kai-red)' : 'var(--kai-yellow)') }">
{{ g.diff >= 0 ? '+'+Math.round(g.diff) : Math.round(g.diff) }}
</td>
<td>
<span class="kai-prio-badge" :class="{'kai-prio-crit':g.prio==='CRIT', 'kai-prio-imp':g.prio==='IMP', 'kai-prio-need':g.prio==='NEED', 'kai-prio-ahead':g.prio==='AHEAD'}">{{ g.prioText }}</span>
<span class="prio-pill" :class="g.prio.toLowerCase()">{{ g.prioText }}</span>
</td>
</tr>
</tbody>
</table>
</div>
<div v-if="data?.strategicFocus?.length">
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:12px;">Стратегические фокусы</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<div v-for="(f, i) in data.strategicFocus" :key="i" class="kai-focus-card">
<div class="kai-focus-num">{{ i+1 }}</div>
<div class="kai-focus-text">{{ f }}</div>
<div class="focus-section" v-if="data?.strategicFocus?.length">
<h4 class="pos-subtitle">Стратегические фокусы</h4>
<div class="focus-list">
<div v-for="(f, i) in data.strategicFocus" :key="i" class="focus-card">
<span class="focus-num">{{ i+1 }}</span>
<span class="focus-text">{{ f }}</span>
</div>
</div>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.pos-grid {
display: grid;
grid-template-columns: 320px 1fr;
gap: 48px;
align-items: start;
}
.radar-col {
display: flex;
flex-direction: column;
gap: 24px;
}
.radar-container {
width: 320px;
height: 320px;
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 24px;
padding: 20px;
position: relative;
}
.radar-svg {
width: 100%;
height: 100%;
overflow: visible;
}
.radar-web {
fill: none;
stroke: var(--kai-border);
stroke-width: 0.5;
}
.radar-axis-line {
stroke: var(--kai-border);
stroke-width: 1;
}
.radar-axis-label {
font-size: 8.5px;
font-weight: 800;
fill: var(--kai-txt3);
text-transform: uppercase;
letter-spacing: 0.05em;
}
.radar-path {
fill-opacity: 0.2;
stroke-width: 2.5;
stroke-linejoin: round;
transition: all 0.6s ease-out;
}
.radar-path.market {
fill: var(--kai-blue);
stroke: var(--kai-blue);
}
.radar-path.user {
fill: var(--kai-green);
stroke: var(--kai-green);
stroke-width: 3.5;
filter: drop-shadow(0 0 8px rgba(var(--kai-green-rgb), 0.3));
}
.radar-bench-label {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
fill: var(--kai-blue);
opacity: 0.4;
}
.radar-legend {
display: flex;
justify-content: center;
gap: 20px;
}
.legend-item {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
font-weight: 700;
color: var(--kai-txt2);
}
.legend-item .dot {
width: 8px;
height: 8px;
border-radius: 2px;
}
.legend-item.market .dot { background: var(--kai-blue); }
.legend-item.user .dot { background: var(--kai-green); }
/* Table Section */
.pos-subtitle {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
margin-bottom: 16px;
}
.table-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 16px;
overflow: hidden;
margin-bottom: 32px;
}
.param-cell {
font-weight: 700;
font-size: 11.5px;
color: var(--kai-txt);
}
.gap-cell {
font-family: 'Unbounded', sans-serif;
font-size: 11px;
font-weight: 800;
}
.prio-pill {
padding: 3px 8px;
border-radius: 4px;
font-size: 8px;
font-weight: 900;
letter-spacing: 0.05em;
}
.prio-pill.crit { color: #fff; background: var(--kai-red); }
.prio-pill.imp { background: rgba(var(--kai-orange-rgb), 0.2); color: var(--kai-orange); }
.prio-pill.need { background: rgba(var(--kai-yellow-rgb), 0.2); color: var(--kai-yellow); }
.prio-pill.ahead { background: rgba(var(--kai-green-rgb), 0.2); color: var(--kai-green); }
/* Focus */
.focus-list {
display: flex;
flex-direction: column;
gap: 10px;
}
.focus-card {
background: rgba(var(--kai-blue-rgb), 0.03);
border: 1px solid var(--kai-border);
padding: 12px 16px;
border-radius: 12px;
display: flex;
align-items: center;
gap: 16px;
transition: all 0.3s;
}
.focus-card:hover {
background: rgba(var(--kai-blue-rgb), 0.06);
border-color: var(--kai-blue);
}
.focus-num {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
color: var(--kai-blue);
width: 24px;
height: 24px;
background: rgba(var(--kai-blue-rgb), 0.1);
border-radius: 6px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.focus-text {
font-size: 11.5px;
font-weight: 600;
color: var(--kai-txt2);
line-height: 1.4;
}
@media (max-width: 1200px) {
.pos-grid {
grid-template-columns: 1fr;
gap: 32px;
}
.radar-col {
align-items: center;
}
}
</style>
@@ -11,37 +11,245 @@ const pars = computed(() => {
</script>
<template>
<div :id="id" class="kai-rationale-section">
<div :id="id" class="rationale-container">
<div class="kai-rationale-content">
<div class="kai-rationale-header">Итоговый план действий</div>
<div class="kai-rationale-title">Почему SMM это ваш следующий шаг</div>
<div class="kai-rationale-subtitle">Обоснование стратегии на основе данных анализа</div>
<div class="rationale-content">
<div class="stamp-badge">
<i class="pi pi-verified mr-2"></i> KAI AI STRATEGY CORE
</div>
<div class="kai-rationale-body">
<p v-for="(p, i) in pars" :key="i">{{ p }}</p>
<div v-if="!pars.length" style="color:rgba(255,255,255,0.3);font-style:italic;">Стратегия формируется...</div>
<div class="content-header">
<h2 class="content-title">Почему SMM ваш следующий шаг</h2>
<p class="content-subtitle">Обоснование стратегии на основе рыночных данных</p>
</div>
<div class="rationale-body">
<div v-for="(p, i) in pars" :key="i" class="rationale-para">
<span class="para-dot"></span>
<p>{{ p }}</p>
</div>
<div v-if="!pars.length" class="empty-pars">Формирование стратегического обоснования...</div>
</div>
</div>
<!-- CTA Block -->
<div class="kai-rationale-cta-block">
<div style="font-family:Unbounded,sans-serif;font-size:24px;font-weight:900;color:#fff;margin-bottom:8px;text-align:center;">🎯 Готовы к запуску?</div>
<div style="text-align:center;color:rgba(255,255,255,0.7);font-size:14px;margin-bottom:24px;">На основе этого анализа мы создадим для вас:</div>
<div class="cta-block">
<div class="cta-inner">
<div class="cta-header">
<div class="cta-icon-box">
<i class="pi pi-bolt"></i>
</div>
<h3 class="cta-title">Готовы к запуску?</h3>
</div>
<p class="cta-desc">На основе этого анализа мы создадим для вас полный план захвата рынка:</p>
<div style="display:flex;flex-direction:column;gap:12px;margin-bottom:32px;max-width:320px;margin-left:auto;margin-right:auto;">
<div style="display:flex;align-items:center;gap:12px;"><span style="color:#00D48B;"></span> Контент-стратегию на 3 месяца</div>
<div style="display:flex;align-items:center;gap:12px;"><span style="color:#00D48B;"></span> Шаблоны для видео и постов</div>
<div style="display:flex;align-items:center;gap:12px;"><span style="color:#00D48B;"></span> График с лучшим временем публикаций</div>
<div style="display:flex;align-items:center;gap:12px;"><span style="color:#00D48B;"></span> Скрипты для ответов в Direct</div>
<div class="cta-features">
<div class="feature-item"><i class="pi pi-check"></i> Контент-стратегия на 3 месяца</div>
<div class="feature-item"><i class="pi pi-check"></i> Шаблоны для видео и постов</div>
<div class="feature-item"><i class="pi pi-check"></i> График публикаций (Prime Time)</div>
<div class="feature-item"><i class="pi pi-check"></i> Скрипты для ответов в CRM</div>
</div>
<div style="text-align:center;">
<button class="kai-cta-btn" style="font-size:16px;padding:16px 36px;" @click="$emit('strategy')">
Создать SMM-стратегию
<button class="prime-cta-btn" @click="$emit('strategy')">
Сгенерировать SMM-стратегию
<i class="pi pi-arrow-right ml-2"></i>
</button>
</div>
</div>
</div>
</template>
<style scoped>
.rationale-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 48px;
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 32px;
padding: 48px;
position: relative;
overflow: hidden;
margin-top: 64px;
}
.rationale-container::before {
content: '';
position: absolute;
top: -100px;
right: -100px;
width: 300px;
height: 300px;
background: radial-gradient(circle, rgba(var(--kai-blue-rgb), 0.1) 0%, transparent 70%);
pointer-events: none;
}
.stamp-badge {
display: inline-flex;
align-items: center;
padding: 6px 12px;
background: rgba(var(--kai-blue-rgb), 0.15);
color: var(--kai-blue);
border-radius: 8px;
font-family: 'Unbounded', sans-serif;
font-size: 8px;
font-weight: 800;
letter-spacing: 0.1em;
margin-bottom: 24px;
border: 1px solid rgba(var(--kai-blue-rgb), 0.2);
}
.content-title {
font-family: 'Unbounded', sans-serif;
font-size: 24px;
font-weight: 800;
color: var(--kai-txt);
margin-bottom: 8px;
line-height: 1.2;
}
.content-subtitle {
font-size: 14px;
color: var(--kai-txt3);
margin-bottom: 40px;
}
.rationale-body {
display: flex;
flex-direction: column;
gap: 20px;
}
.rationale-para {
display: flex;
gap: 16px;
}
.para-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--kai-blue);
margin-top: 8px;
flex-shrink: 0;
opacity: 0.5;
}
.rationale-para p {
font-size: 13px;
line-height: 1.6;
color: var(--kai-txt2);
margin: 0;
}
.empty-pars {
color: var(--kai-txt3);
font-style: italic;
font-size: 12px;
}
/* CTA */
.cta-block {
display: flex;
align-items: center;
}
.cta-inner {
background: rgba(255,255,255,0.03);
border: 1px solid var(--kai-border);
padding: 40px;
border-radius: 24px;
width: 100%;
}
.cta-header {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
margin-bottom: 12px;
}
.cta-icon-box {
width: 56px;
height: 56px;
background: var(--kai-blue);
color: white;
border-radius: 16px;
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
box-shadow: 0 8px 16px rgba(var(--kai-blue-rgb), 0.3);
}
.cta-title {
font-family: 'Unbounded', sans-serif;
font-size: 20px;
font-weight: 800;
color: var(--kai-txt);
margin: 0;
text-align: center;
}
.cta-desc {
font-size: 12px;
color: var(--kai-txt3);
text-align: center;
margin-bottom: 24px;
}
.cta-features {
display: flex;
flex-direction: column;
gap: 10px;
margin-bottom: 32px;
}
.feature-item {
display: flex;
align-items: center;
gap: 12px;
font-size: 11.5px;
font-weight: 600;
color: var(--kai-txt2);
}
.feature-item i {
color: var(--kai-green);
font-size: 10px;
}
.prime-cta-btn {
width: 100%;
background: var(--kai-blue);
color: white;
border: none;
padding: 16px;
border-radius: 14px;
font-family: 'Unbounded', sans-serif;
font-size: 12px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
cursor: pointer;
transition: all 0.3s;
display: flex;
align-items: center;
justify-content: center;
}
.prime-cta-btn:hover {
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(var(--kai-blue-rgb), 0.4);
background: #3b8dff;
}
@media (max-width: 992px) {
.rationale-container {
grid-template-columns: 1fr;
padding: 32px;
}
}
</style>
@@ -5,61 +5,311 @@ import { computed } from 'vue';
const props = defineProps({ id: String, data: Object });
const fmtNum = (n) => n != null ? Number(n).toLocaleString('ru-RU') : '—';
const fmtPct = (n) => n != null ? (parseFloat(n) * 100).toFixed(0) + '%' : '—';
const stars = (r) => { const n = parseFloat(r)||0; return '★'.repeat(Math.round(n))+'☆'.repeat(5-Math.round(n)); };
const stars = (r) => {
const n = Math.round(parseFloat(r) || 0);
return '★'.repeat(n) + '☆'.repeat(5 - n);
};
const starDist = computed(() => {
const d = props.data?.starDistribution || {};
const max = Math.max(...Object.values(d).map(v => parseFloat(v) || 0), 1);
return [5,4,3,2,1].map(n => ({ star: n, pct: parseFloat(d[String(n)]) || 0, fill: max, color: ['#22C55E','#86EFAC','#F59E0B','#F97316','#EF4444'][5-n] }));
return [5,4,3,2,1].map(n => ({
star: n,
pct: parseFloat(d[String(n)]) || 0,
color: ['var(--kai-green)','var(--kai-green)','var(--kai-yellow)','var(--kai-orange)','var(--kai-red)'][5-n]
}));
});
</script>
<template>
<SectionWrapper :id="id" title="Репутация ниши" subtitle="Насколько клиенты доверяют бизнесам в вашей сфере" num="06">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:24px;">
<!-- Metrics -->
<div style="display:flex;flex-direction:column;gap:12px;">
<div style="display:grid;grid-template-columns:repeat(3,1fr);gap:10px;">
<SectionWrapper :id="id" title="Репутация ниши" subtitle="Анализ доверия и качества обслуживания в сегменте" num="6">
<div class="rep-grid">
<!-- LEFT: METRICS -->
<div class="rep-metrics-col">
<div class="rep-cards-row">
<div v-for="card in [
{ icon:'', val:(data?.avgNicheRating||0).toFixed(1), label:'Ср. рейтинг', sub: stars(data?.avgNicheRating), color:'#F59E0B', tip: 'Средняя оценка конкурентов (например, в 2ГИС, Яндекс)' },
{ icon:'💬', val: fmtNum(data?.medianReviews), label:'Медиана отзывов', sub:'типичное кол-во у бизнеса', color:'#2D7BFF', tip: 'Количество отзывов, которое есть у типичного (среднего) игрока на рынке' },
{ icon:'🛡', val: fmtPct(data?.highTrustBusinessShare), label:'Высокое доверие', sub:'бизнесов с рейтингом >4.3', color:'#00D48B', tip: 'Процент компаний, которые имеют высокий уровень доверия клиентов' },
]" :key="card.label" style="background:rgba(255,255,255,0.03);border:1px solid rgba(255,255,255,0.07);border-radius:12px;padding:14px;text-align:center;">
<div style="font-size:22px;margin-bottom:6px;">{{ card.icon }}</div>
<div style="font-family:Unbounded,sans-serif;font-size:22px;font-weight:900;line-height:1;margin-bottom:4px;" :style="{ color: card.color }">{{ card.val }}</div>
<div style="font-size:10px;font-weight:700;text-transform:uppercase;letter-spacing:.05em;color:rgba(255,255,255,0.4);margin-bottom:2px;">
{ icon:'pi-star-fill', val:(data?.avgNicheRating||0).toFixed(1), label:'Ср. рейтинг', sub: stars(data?.avgNicheRating), color:'var(--kai-yellow)', tip: 'Средняя оценка конкурентов в нише' },
{ icon:'pi-comments', val: fmtNum(data?.medianReviews), label:'Медиана отзывов', sub:'на 1 бизнес', color:'var(--kai-blue)', tip: 'Типичное количество отзывов у игрока на рынке' },
{ icon:'pi-shield', val: fmtPct(data?.highTrustBusinessShare), label:'High Trust', sub:'>4.3 rating', color:'var(--kai-green)', tip: 'Доля компаний с высоким уровнем доверия' },
]" :key="card.label" class="rep-metric-card">
<div class="rep-card-icon" :style="{ color: card.color, background: card.color + '15' }">
<i class="pi" :class="card.icon"></i>
</div>
<div class="rep-card-val" :style="{ color: card.color }">{{ card.val }}</div>
<div class="rep-card-label">
{{ card.label }}
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="card.tip"></i>
</div>
<div style="font-size:11px;color:rgba(255,255,255,0.5);">{{ card.sub }}</div>
<div class="rep-card-sub" :class="{ 'star-sub': card.icon === 'pi-star-fill' }">{{ card.sub }}</div>
</div>
</div>
<!-- Response time -->
<div style="display:flex;align-items:center;gap:14px;padding:14px 16px;background:rgba(255,255,255,0.02);border:1px solid rgba(255,255,255,0.06);border-radius:12px;">
<span style="font-size:24px;"></span>
<div>
<div style="font-size:11px;color:rgba(255,255,255,0.4);font-weight:600;text-transform:uppercase;letter-spacing:.05em;">
<div class="response-box">
<div class="response-icon">
<i class="pi pi-clock"></i>
</div>
<div class="response-info">
<div class="response-label">
Ср. время ответа владельца
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Как быстро конкуренты реагируют на новые отзывы клиентов'"></i>
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Скорость реакции бизнеса на отзывы клиентов'"></i>
</div>
<div style="font-family:Unbounded,sans-serif;font-size:20px;font-weight:900;color:rgba(255,255,255,0.9);">{{ data?.avgOwnerResponseTimeHours || '—' }} <span style="font-size:12px;font-weight:400;">часов</span></div>
<div class="response-val">
{{ data?.avgOwnerResponseTimeHours || '—' }}
<span class="unit">часов</span>
</div>
</div>
<div class="response-status" :style="{ color: parseFloat(data?.avgOwnerResponseTimeHours) <= 24 ? 'var(--kai-green)' : 'var(--kai-orange)' }">
<i class="pi" :class="parseFloat(data?.avgOwnerResponseTimeHours) <= 24 ? 'pi-check-circle' : 'pi-exclamation-circle'"></i>
</div>
</div>
</div>
<!-- Star distribution -->
<div>
<div style="font-size:11px;font-weight:700;text-transform:uppercase;letter-spacing:.08em;color:rgba(255,255,255,0.4);margin-bottom:14px;">Распределение оценок</div>
<div style="display:flex;flex-direction:column;gap:10px;">
<div v-for="s in starDist" :key="s.star" style="display:flex;align-items:center;gap:10px;">
<span style="font-size:12px;font-weight:700;width:28px;text-align:right;" :style="{ color: s.color }">{{ s.star }}</span>
<div class="kai-progress-track" style="flex:1;height:10px;">
<div class="kai-progress-fill" :style="{ width: s.pct + '%', background: s.color, maxWidth:'100%' }" />
<!-- RIGHT: STAR DISTRIBUTION -->
<div class="rep-dist-col">
<h4 class="dist-title">Распределение оценок</h4>
<div class="dist-list">
<div v-for="s in starDist" :key="s.star" class="dist-item">
<div class="dist-label" :style="{ color: s.color }">{{ s.star }}</div>
<div class="kai-progress-track">
<div class="kai-progress-fill" :style="{ width: s.pct + '%', background: s.color }" />
</div>
<span style="font-size:12px;font-weight:700;width:36px;" :style="{ color: s.color }">{{ s.pct.toFixed(1) }}%</span>
<div class="dist-pct" :style="{ color: s.color }">{{ s.pct.toFixed(1) }}%</div>
</div>
</div>
<div class="dist-summary">
<div class="summary-line">
<span class="dot green"></span>
<span class="text">Положительные (4-5)</span>
<span class="val">{{ (starDist[0].pct + starDist[1].pct).toFixed(1) }}%</span>
</div>
<div class="summary-line">
<span class="dot red"></span>
<span class="text">Отрицательные (1-2)</span>
<span class="val">{{ (starDist[3].pct + starDist[4].pct).toFixed(1) }}%</span>
</div>
</div>
</div>
</div>
</SectionWrapper>
</template>
<style scoped>
.rep-grid {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 40px;
align-items: start;
}
.rep-metrics-col {
display: flex;
flex-direction: column;
gap: 24px;
}
.rep-cards-row {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.rep-metric-card {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
padding: 20px 16px;
border-radius: 16px;
text-align: center;
transition: all 0.3s;
}
.rep-metric-card:hover {
transform: translateY(-2px);
border-color: var(--kai-border-hover);
background: var(--kai-card-hover);
}
.rep-card-icon {
width: 32px;
height: 32px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 12px;
}
.rep-card-icon i {
font-size: 14px;
}
.rep-card-val {
font-family: 'Unbounded', sans-serif;
font-size: 18px;
font-weight: 800;
margin-bottom: 4px;
}
.rep-card-label {
font-size: 9.5px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.05em;
margin-bottom: 6px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.rep-card-sub {
font-size: 10px;
color: var(--kai-txt3);
opacity: 0.6;
}
.star-sub {
color: var(--kai-yellow) !important;
font-size: 12px !important;
letter-spacing: 2px;
opacity: 1 !important;
}
/* Response time box */
.response-box {
display: flex;
align-items: center;
gap: 20px;
padding: 20px 24px;
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 16px;
}
.response-icon {
width: 44px;
height: 44px;
border-radius: 12px;
background: rgba(var(--kai-blue-rgb), 0.1);
color: var(--kai-blue);
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
}
.response-info {
flex: 1;
}
.response-label {
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
letter-spacing: 0.05em;
margin-bottom: 4px;
}
.response-val {
font-family: 'Unbounded', sans-serif;
font-size: 20px;
font-weight: 800;
color: var(--kai-txt);
}
.response-val .unit {
font-size: 12px;
font-weight: 400;
color: var(--kai-txt3);
margin-left: 4px;
}
.response-status {
font-size: 20px;
}
/* Dist Col */
.dist-title {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
color: var(--kai-txt3);
margin-bottom: 24px;
}
.dist-list {
display: flex;
flex-direction: column;
gap: 12px;
margin-bottom: 24px;
}
.dist-item {
display: grid;
grid-template-columns: 32px 1fr 48px;
align-items: center;
gap: 12px;
}
.dist-label {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-align: right;
}
.dist-pct {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-align: right;
}
.dist-summary {
display: flex;
flex-direction: column;
gap: 12px;
padding: 16px;
background: rgba(0,0,0,0.15);
border-radius: 12px;
border: 1px solid var(--kai-border);
}
.summary-line {
display: flex;
align-items: center;
gap: 10px;
}
.summary-line .dot {
width: 6px;
height: 6px;
border-radius: 50%;
}
.summary-line .dot.green { background: var(--kai-green); }
.summary-line .dot.red { background: var(--kai-red); }
.summary-line .text {
font-size: 10.5px;
color: var(--kai-txt2);
flex: 1;
}
.summary-line .val {
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
color: var(--kai-txt);
}
@media (max-width: 992px) {
.rep-grid {
grid-template-columns: 1fr;
}
}
</style>
@@ -1,26 +1,151 @@
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const sections = [
{ id: 's0', label: '0' }, { id: 's1', label: '1' }, { id: 's2', label: '2' },
{ id: 's3', label: '3' }, { id: 's4', label: '4' }, { id: 's5', label: '5' },
{ id: 's6', label: '6' }, { id: 's7', label: '7' }, { id: 's8', label: '8' },
{ id: 's9', label: '9' }, { id: 's10', label: '10' }, { id: 's11', label: '11' },
{ id: 's0', label: 'Overview' },
{ id: 's1', label: 'Landscape' },
{ id: 's2', label: 'Geometry' },
{ id: 's3', label: 'Competitors' },
{ id: 's4', label: 'Content' },
{ id: 's5', label: 'Intensity' },
{ id: 's6', label: 'Reputation' },
{ id: 's7', label: 'Behavior' },
{ id: 's8', label: 'Demand' },
{ id: 's9', label: 'Positioning' },
{ id: 's10', label: 'Summary' },
{ id: 's11', label: 'Rationale' },
];
const active = ref('s0');
const go = (id) => document.getElementById(id)?.scrollIntoView({ behavior: 'smooth' });
const go = (id) => {
const el = document.getElementById(id);
if (el) {
window.scrollTo({
top: el.offsetTop - 100,
behavior: 'smooth'
});
}
};
let obs;
onMounted(() => {
obs = new IntersectionObserver(entries => {
entries.forEach(e => { if (e.isIntersecting) active.value = e.target.id; });
}, { threshold: 0.3 });
sections.forEach(s => { const el = document.getElementById(s.id); if (el) obs.observe(el); });
entries.forEach(e => {
if (e.isIntersecting) active.value = e.target.id;
});
}, { threshold: 0.3, rootMargin: '-10% 0px -70% 0px' });
sections.forEach(s => {
const el = document.getElementById(s.id);
if (el) obs.observe(el);
});
});
onUnmounted(() => obs?.disconnect());
</script>
<template>
<nav class="kai-sidenav">
<button v-for="s in sections" :key="s.id" class="kai-sidenav-dot" :class="{ active: active === s.id }"
:title="s.label" @click="go(s.id)" />
<div class="sidenav-inner">
<button v-for="(s, i) in sections" :key="s.id"
class="sidenav-item"
:class="{ active: active === s.id }"
@click="go(s.id)">
<span class="sidenav-num">{{ i }}</span>
<span class="sidenav-label">{{ s.label }}</span>
</button>
</div>
</nav>
</template>
<style scoped>
.kai-sidenav {
position: fixed;
left: 24px;
top: 50%;
transform: translateY(-50%);
z-index: 100;
display: flex;
flex-direction: column;
pointer-events: none;
}
.sidenav-inner {
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: auto;
padding: 12px 8px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border: 1px solid var(--kai-border);
border-radius: 16px;
}
.sidenav-item {
width: 28px;
height: 28px;
border-radius: 8px;
background: transparent;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: visible;
}
.sidenav-num {
font-family: 'Unbounded', sans-serif;
font-size: 8px;
font-weight: 800;
color: var(--kai-txt3);
transition: all 0.3s;
}
.sidenav-label {
position: absolute;
left: 40px;
background: var(--kai-bg);
border: 1px solid var(--kai-border);
color: var(--kai-txt);
padding: 4px 10px;
border-radius: 6px;
font-size: 10px;
font-weight: 700;
white-space: nowrap;
opacity: 0;
transform: translateX(-10px);
pointer-events: none;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
.sidenav-item:hover .sidenav-label {
opacity: 1;
transform: translateX(0);
}
.sidenav-item:hover {
background: var(--kai-card-hover);
border-color: var(--kai-border);
}
.sidenav-item.active {
background: var(--kai-blue);
border-color: var(--kai-blue);
box-shadow: 0 0 15px rgba(var(--kai-blue-rgb), 0.4);
}
.sidenav-item.active .sidenav-num {
color: #fff;
}
@media (max-width: 1400px) {
.kai-sidenav {
display: none;
}
}
</style>
@@ -37,22 +37,28 @@ const toggle = (value) => {
</script>
<template>
<div class="field md:col-span-2 space-y-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-300">{{ label }}</p>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="field md:col-span-2 space-y-1.5">
<p class="text-[10px] font-black uppercase tracking-widest text-surface-500 dark:text-surface-400 mb-2">{{ label }}</p>
<div class="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
<div
v-for="option in options"
:key="option.value"
class="flex items-start gap-2 rounded-xl border px-4 py-3 text-left text-sm transition cursor-pointer"
class="flex items-center gap-2 rounded-xl border px-3 py-2.5 text-left text-[11.5px] transition-all cursor-pointer relative overflow-hidden"
:class="{
'border-primary-500 bg-primary-50 text-primary-900 shadow-sm dark:border-primary-400 dark:bg-primary-900/30 dark:text-primary-50': isSelected(option.value),
'border-surface-200 bg-surface-0 text-surface-900 hover:border-primary-200 hover:bg-primary-50/40 dark:border-surface-700 dark:bg-surface-900 dark:text-surface-100 dark:hover:border-primary-500/60': !isSelected(option.value)
'border-emerald-500/50 bg-emerald-500/10 text-emerald-500 ring-1 ring-emerald-500/20 shadow-[0_0_15px_rgba(16,185,129,0.1)]': isSelected(option.value),
'border-surface-200/50 bg-white/5 text-surface-400 hover:border-emerald-500/30 hover:bg-white/10 dark:border-surface-800/50 dark:bg-black/20': !isSelected(option.value)
}"
@click="toggle(option.value)"
>
<span class="font-medium">{{ option.label }}</span>
<div v-if="isSelected(option.value)" class="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse shadow-[0_0_8px_var(--kai-green)]"></div>
<div v-else class="w-1.5 h-1.5 rounded-full bg-surface-700/50"></div>
<span class="font-bold tracking-tight">{{ option.label }}</span>
<div v-if="isSelected(option.value)" class="absolute top-0 right-0 p-1 opacity-20">
<i class="pi pi-check text-[10px]" />
</div>
</div>
<small v-if="error" class="p-error">{{ error }}</small>
</div>
<small v-if="error" class="text-[10px] font-bold text-red-500 mt-1 uppercase tracking-tight">{{ error }}</small>
</div>
</template>
@@ -22,22 +22,29 @@ defineEmits(['update:modelValue']);
</script>
<template>
<div class="field md:col-span-2 space-y-2">
<p class="text-xs font-medium text-surface-600 dark:text-surface-300">{{ label }}</p>
<div class="grid grid-cols-1 gap-2 sm:grid-cols-2">
<div class="field md:col-span-2 space-y-1.5">
<p class="text-[10px] font-black uppercase tracking-widest text-surface-500 dark:text-surface-400 mb-2">{{ label }}</p>
<div class="grid grid-cols-1 gap-1.5 sm:grid-cols-2">
<div
v-for="option in options"
:key="option.value"
class="flex items-start gap-2 rounded-xl border px-4 py-3 text-left text-sm transition cursor-pointer"
class="flex items-center gap-2 rounded-xl border px-3 py-2.5 text-left text-[11.5px] transition-all cursor-pointer relative overflow-hidden"
:class="{
'border-primary-500 bg-primary-50 text-primary-900 shadow-sm dark:border-primary-400 dark:bg-primary-900/30 dark:text-primary-50': modelValue === option.value,
'border-surface-200 bg-surface-0 text-surface-900 hover:border-primary-200 hover:bg-primary-50/40 dark:border-surface-700 dark:bg-surface-900 dark:text-surface-100 dark:hover:border-primary-500/60': modelValue !== option.value
'border-blue-500/50 bg-blue-500/10 text-blue-500 ring-1 ring-blue-500/20 shadow-[0_0_15px_rgba(45,123,255,0.1)]': modelValue === option.value,
'border-surface-200/50 bg-white/5 text-surface-400 hover:border-blue-500/30 hover:bg-white/10 dark:border-surface-800/50 dark:bg-black/20': modelValue !== option.value
}"
@click="$emit('update:modelValue', option.value)"
>
<span class="font-medium">{{ option.label }}</span>
<div v-if="modelValue === option.value" class="w-1.5 h-1.5 rounded-full bg-blue-500 animate-pulse shadow-[0_0_8px_var(--kai-blue)]"></div>
<div v-else class="w-1.5 h-1.5 rounded-full bg-surface-700/50"></div>
<span class="font-bold tracking-tight">{{ option.label }}</span>
<!-- Subtle indicator for selected -->
<div v-if="modelValue === option.value" class="absolute top-0 right-0 p-1 opacity-20">
<i class="pi pi-check text-[10px]" />
</div>
</div>
<small v-if="error" class="p-error">{{ error }}</small>
</div>
<small v-if="error" class="text-[10px] font-bold text-red-500 mt-1 uppercase tracking-tight">{{ error }}</small>
</div>
</template>
@@ -8,11 +8,12 @@ defineProps({
</script>
<template>
<section class="space-y-4">
<h2 class="text-sm font-semibold uppercase tracking-wider text-surface-500 dark:text-surface-400">
<section class="space-y-3 pb-8 border-b border-surface-200/50 dark:border-surface-800/50 last:border-b-0">
<h2 class="text-[10px] font-black uppercase tracking-[0.2em] text-blue-500/80 mb-4 flex items-center gap-2">
<span class="w-1 h-3 bg-blue-500 rounded-full"></span>
{{ title }}
</h2>
<div class="grid grid-cols-1 gap-4 md:grid-cols-2">
<div class="grid grid-cols-1 gap-5 md:grid-cols-2">
<slot />
</div>
</section>
+186 -22
View File
@@ -29,7 +29,7 @@ const status = ref('IDLE');
const result = ref(null);
const errorMsg = ref('');
const MSGS = ['Собираем данные конкурентов...','Анализируем отзывы и рейтинги...','Считаем ER и частоту публикаций...','Строим карту контент-стратегий...','Генерируем выводы и приоритеты...'];
const MSGS = ['Data Ingestion: Active','Contextual Mapping...','Competitor Core Analysis...','Sentiment Extraction...','Synthesis & Prioritization...'];
const loaderMsg = ref(MSGS[0]);
const loaderDot = ref(1);
let pollInterval = null;
@@ -92,7 +92,7 @@ onUnmounted(() => stopPolling());
</script>
<template>
<div class="kai-page">
<div class="marketing-analysis-v3 kai-page">
<!-- Sticky Header -->
<V4Header :status="status" :exec="exec" @strategy="goStrategy" @back="$router.push('/marketing-analysis/v3')" />
@@ -100,33 +100,39 @@ onUnmounted(() => stopPolling());
<!-- Side Nav -->
<V4SideNav v-if="status === 'COMPLETED'" />
<div style="max-width:1280px; margin:0 auto; padding:28px 24px 80px;">
<div class="analysis-container">
<!-- LOADING -->
<div v-if="loading || status === 'QUEUED' || status === 'PROCESSING'" style="display:flex;flex-direction:column;align-items:center;justify-content:center;padding:120px 0;gap:24px;">
<div style="position:relative;width:80px;height:80px;">
<div style="position:absolute;inset:0;border:3px solid rgba(45,123,255,0.15);border-radius:50%;"></div>
<div style="position:absolute;inset:0;border:3px solid transparent;border-top-color:#2D7BFF;border-radius:50%;animation:spin 0.8s linear infinite;"></div>
<!-- LOADING STATE -->
<div v-if="loading || status === 'QUEUED' || status === 'PROCESSING'" class="loading-state">
<div class="loader-wrap">
<div class="loader-circle"></div>
<div class="loader-inner-circle"></div>
<div class="kai-logo loader-logo">KAI</div>
</div>
<div style="text-align:center;">
<p style="font-family:Unbounded,sans-serif;font-size:16px;font-weight:700;color:rgba(255,255,255,0.9);margin-bottom:8px;">{{ loaderMsg }}</p>
<p style="font-size:13px;color:rgba(255,255,255,0.4);max-width:360px;">Система ИИ собирает данные. Вы можете оставить вкладку данные появятся автоматически.</p>
<div class="loading-info">
<div class="loading-title">{{ loaderMsg }}</div>
<p class="loading-sub">KAI Prime Engine is synthesising market intelligence...</p>
<div class="loading-dots">
<span v-for="i in 5" :key="i" class="dot" :class="{ active: loaderDot === i }"></span>
</div>
<div style="display:flex;gap:8px;">
<span v-for="i in 5" :key="i" :style="{ width:'10px', height:'10px', borderRadius:'50%', background: loaderDot===i ? '#2D7BFF' : 'rgba(255,255,255,0.1)', display:'inline-block', transition:'all .3s' }" />
</div>
</div>
<!-- ERROR -->
<div v-else-if="status === 'FAILED'" style="display:flex;flex-direction:column;align-items:center;padding:100px 0;gap:20px;text-align:center;">
<div style="width:80px;height:80px;border-radius:50%;background:rgba(239,68,68,0.1);border:1px solid rgba(239,68,68,0.3);display:flex;align-items:center;justify-content:center;font-size:36px;"></div>
<p style="font-family:Unbounded,sans-serif;font-size:20px;font-weight:700;color:rgba(255,255,255,0.9);">Сбой генерации</p>
<p style="font-size:14px;color:rgba(255,255,255,0.5);max-width:400px;">{{ errorMsg || 'Непредвиденная ошибка. Попробуйте ещё раз.' }}</p>
<button class="kai-pdf-btn" @click="$router.push('/marketing-analysis/v3')"> Вернуться к списку</button>
<!-- ERROR STATE -->
<div v-else-if="status === 'FAILED'" class="error-state">
<div class="error-icon">
<i class="pi pi-exclamation-triangle" />
</div>
<h2 class="error-title">Analysis Interrupted</h2>
<p class="error-desc">{{ errorMsg || 'An unexpected error occurred during intelligence synthesis.' }}</p>
<button class="kai-btn-secondary" @click="$router.push('/marketing-analysis/v3')">
<i class="pi pi-arrow-left mr-2" /> Return to Command Center
</button>
</div>
<!-- DASHBOARD -->
<!-- DASHBOARD CONTENT -->
<template v-else-if="status === 'COMPLETED' && result">
<div class="dashboard-grid">
<V4Executive id="s0" :data="exec" />
<V4Landscape id="s1" :data="land" :demand="dem" />
<V4Geo id="s2" :data="geo" />
@@ -139,11 +145,169 @@ onUnmounted(() => stopPolling());
<V4Positioning id="s9" :data="pos" />
<V4Conclusions id="s10" :data="concl" />
<V4Rationale id="s11" :data="rat" @strategy="goStrategy" />
</div>
</template>
</div>
</div>
</template>
<style>
@keyframes spin { to { transform:rotate(360deg); } }
<style scoped>
.analysis-container {
max-width: 1200px;
margin: 0 auto;
padding: 32px 24px 100px;
}
.dashboard-grid {
display: flex;
flex-direction: column;
gap: 32px;
}
/* Loading State Stylings */
.loading-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 140px 0;
gap: 32px;
}
.loader-wrap {
position: relative;
width: 100px;
height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.loader-circle {
position: absolute;
inset: 0;
border: 2px solid var(--kai-border);
border-radius: 50%;
}
.loader-inner-circle {
position: absolute;
inset: 0;
border: 2px solid transparent;
border-top-color: var(--kai-blue);
border-radius: 50%;
animation: spin 1s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
.loader-logo {
font-size: 11px;
z-index: 2;
box-shadow: 0 0 20px rgba(var(--kai-blue-rgb), 0.4);
}
.loading-info {
text-align: center;
}
.loading-title {
font-family: 'Unbounded', sans-serif;
font-size: 14px;
font-weight: 800;
color: var(--kai-txt);
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 0.1em;
}
.loading-sub {
font-size: 11px;
color: var(--kai-txt3);
max-width: 300px;
margin: 0 auto 16px;
}
.loading-dots {
display: flex;
justify-content: center;
gap: 6px;
}
.loading-dots .dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--kai-border);
transition: all 0.3s;
}
.loading-dots .dot.active {
background: var(--kai-blue);
transform: scale(1.4);
box-shadow: 0 0 8px var(--kai-blue);
}
/* Error State Stylings */
.error-state {
display: flex;
flex-direction: column;
align-items: center;
padding: 120px 0;
gap: 20px;
text-align: center;
}
.error-icon {
width: 64px;
height: 64px;
border-radius: 16px;
background: rgba(239, 68, 68, 0.08);
border: 1px solid rgba(239, 68, 68, 0.2);
display: flex;
align-items: center;
justify-content: center;
color: var(--kai-red);
font-size: 24px;
margin-bottom: 8px;
}
.error-title {
font-family: 'Unbounded', sans-serif;
font-size: 18px;
font-weight: 900;
color: var(--kai-txt);
letter-spacing: -0.02em;
}
.error-desc {
font-size: 12px;
color: var(--kai-txt3);
max-width: 360px;
line-height: 1.6;
}
.kai-btn-secondary {
margin-top: 12px;
background: transparent;
border: 1px solid var(--kai-border);
color: var(--kai-txt2);
padding: 10px 20px;
border-radius: 12px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: all 0.3s;
cursor: pointer;
}
.kai-btn-secondary:hover {
background: var(--kai-card-hover);
border-color: var(--kai-border-hover);
color: var(--kai-txt);
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
</style>
+49 -49
View File
@@ -17,10 +17,10 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
<div class="mkt-hub__header">
<div class="mkt-hub__badge">
<span class="mkt-hub__badge-dot"></span>
AI Marketing Suite · v4.0
KAI Intelligence · Prime 2026
</div>
<h1 class="mkt-hub__title">ИИ-Маркетолог</h1>
<p class="mkt-hub__sub">Автономная генерация стратегий, контент-планов и медиа-активов на основе глубокого анализа ниши и конкурентов</p>
<h1 class="mkt-hub__title">Маркетинг-Хаб</h1>
<p class="mkt-hub__sub">Автономное управление маркетингом: от глубоких исследований рынка до генерации контента и запуска рекламных кампаний.</p>
</div>
<!-- GRID -->
@@ -141,22 +141,22 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
.mkt-hub__badge {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 5px 14px;
gap: 6px;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid rgba(45,123,255,0.3);
background: rgba(45,123,255,0.08);
font-size: 11px;
font-weight: 700;
border: 1px solid var(--kai-border);
background: rgba(var(--kai-blue-rgb), 0.08);
font-size: 9px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.07em;
letter-spacing: 0.1em;
color: var(--kai-blue);
margin-bottom: 20px;
margin-bottom: 16px;
}
.mkt-hub__badge-dot {
width: 6px;
height: 6px;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--kai-blue);
box-shadow: 0 0 8px var(--kai-blue);
@@ -169,20 +169,20 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
.mkt-hub__title {
font-family: 'Unbounded', sans-serif;
font-size: clamp(32px, 5vw, 56px);
font-size: 32px;
font-weight: 900;
color: var(--kai-txt);
letter-spacing: -0.02em;
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 16px;
margin-bottom: 12px;
}
.mkt-hub__sub {
font-size: 16px;
font-size: 13px;
color: var(--kai-txt2);
max-width: 560px;
max-width: 500px;
margin: 0 auto;
line-height: 1.65;
line-height: 1.6;
font-weight: 400;
}
@@ -245,17 +245,17 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
.mkt-card__title {
font-family: 'Unbounded', sans-serif;
font-size: 17px;
font-size: 14px;
font-weight: 700;
color: var(--kai-txt);
margin-bottom: 10px;
margin-bottom: 8px;
line-height: 1.25;
}
.mkt-card__desc {
font-size: 13px;
font-size: 11.5px;
color: var(--kai-txt2);
line-height: 1.6;
line-height: 1.55;
font-weight: 400;
}
@@ -293,25 +293,25 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
.mkt-card__tag {
display: inline-block;
margin-top: 14px;
padding: 3px 10px;
margin-top: 12px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(0,212,139,0.25);
background: rgba(0,212,139,0.07);
border: 1px solid rgba(var(--kai-green-rgb), 0.25);
background: rgba(var(--kai-green-rgb), 0.07);
color: var(--kai-green);
font-size: 10px;
font-weight: 700;
font-size: 8.5px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.06em;
letter-spacing: 0.08em;
}
/* ───── Targetologist Banner ───── */
.mkt-banner {
background: linear-gradient(135deg, rgba(45,123,255,0.12) 0%, rgba(99,51,220,0.08) 100%);
border: 1px solid rgba(45,123,255,0.3);
border-radius: 24px;
padding: 32px 36px;
background: linear-gradient(135deg, rgba(var(--kai-blue-rgb), 0.08) 0%, rgba(139,92,246,0.05) 100%);
border: 1px solid var(--kai-border);
border-radius: 20px;
padding: 24px 32px;
display: flex;
align-items: center;
justify-content: space-between;
@@ -365,17 +365,17 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
font-weight: 700;
font-size: 9px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.08em;
letter-spacing: 0.1em;
color: var(--kai-blue);
margin-bottom: 6px;
margin-bottom: 4px;
}
.mkt-banner__dot {
width: 6px;
height: 6px;
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--kai-blue);
animation: pulse-dot 2s infinite;
@@ -383,30 +383,30 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
.mkt-banner__title {
font-family: 'Unbounded', sans-serif;
font-size: 24px;
font-size: 20px;
font-weight: 900;
color: var(--kai-txt);
margin-bottom: 8px;
margin-bottom: 6px;
line-height: 1.2;
}
.mkt-banner__desc {
font-size: 14px;
font-size: 12px;
color: var(--kai-txt2);
line-height: 1.55;
max-width: 580px;
line-height: 1.5;
max-width: 520px;
}
.mkt-banner__cta {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 12px;
padding: 10px 20px;
border-radius: 10px;
background: var(--kai-blue);
color: #fff;
font-size: 13px;
font-weight: 700;
font-size: 11.5px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
white-space: nowrap;
flex-shrink: 0;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,47 @@
// vite.config.mjs
import { fileURLToPath, URL } from "node:url";
import { PrimeVueResolver } from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/@primevue/auto-import-resolver/index.mjs";
import vue from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/@vitejs/plugin-vue/dist/index.mjs";
import Components from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/unplugin-vue-components/dist/vite.js";
import { defineConfig } from "file:///C:/Users/777/IdeaProjects/marketing/node_modules/vite/dist/node/index.js";
var __vite_injected_original_import_meta_url = "file:///C:/Users/777/IdeaProjects/marketing/vite.config.mjs";
var vite_config_default = defineConfig({
optimizeDeps: {
noDiscovery: true
},
plugins: [
vue(),
Components({
resolvers: [PrimeVueResolver()]
})
],
resolve: {
alias: {
"@": fileURLToPath(new URL("./src", __vite_injected_original_import_meta_url))
}
},
server: {
proxy: {
"/api": {
target: "https://api.konturai.kz",
changeOrigin: true,
secure: true,
configure: (proxy, _options) => {
proxy.on("error", (err, _req, _res) => {
console.log("proxy error", err);
});
proxy.on("proxyReq", (proxyReq, req, _res) => {
console.log("Sending Request to the Target:", req.method, req.url);
});
proxy.on("proxyRes", (proxyRes, req, _res) => {
console.log("Received Response from the Target:", proxyRes.statusCode, req.url);
});
}
}
}
}
});
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcubWpzIl0sCiAgInNvdXJjZXNDb250ZW50IjogWyJjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZGlybmFtZSA9IFwiQzpcXFxcVXNlcnNcXFxcNzc3XFxcXElkZWFQcm9qZWN0c1xcXFxtYXJrZXRpbmdcIjtjb25zdCBfX3ZpdGVfaW5qZWN0ZWRfb3JpZ2luYWxfZmlsZW5hbWUgPSBcIkM6XFxcXFVzZXJzXFxcXDc3N1xcXFxJZGVhUHJvamVjdHNcXFxcbWFya2V0aW5nXFxcXHZpdGUuY29uZmlnLm1qc1wiO2NvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9pbXBvcnRfbWV0YV91cmwgPSBcImZpbGU6Ly8vQzovVXNlcnMvNzc3L0lkZWFQcm9qZWN0cy9tYXJrZXRpbmcvdml0ZS5jb25maWcubWpzXCI7aW1wb3J0IHsgZmlsZVVSTFRvUGF0aCwgVVJMIH0gZnJvbSAnbm9kZTp1cmwnO1xuXG5pbXBvcnQgeyBQcmltZVZ1ZVJlc29sdmVyIH0gZnJvbSAnQHByaW1ldnVlL2F1dG8taW1wb3J0LXJlc29sdmVyJztcbmltcG9ydCB2dWUgZnJvbSAnQHZpdGVqcy9wbHVnaW4tdnVlJztcbmltcG9ydCBDb21wb25lbnRzIGZyb20gJ3VucGx1Z2luLXZ1ZS1jb21wb25lbnRzL3ZpdGUnO1xuaW1wb3J0IHsgZGVmaW5lQ29uZmlnIH0gZnJvbSAndml0ZSc7XG5cbi8vIGh0dHBzOi8vdml0ZWpzLmRldi9jb25maWcvXG5leHBvcnQgZGVmYXVsdCBkZWZpbmVDb25maWcoe1xuICAgIG9wdGltaXplRGVwczoge1xuICAgICAgICBub0Rpc2NvdmVyeTogdHJ1ZVxuICAgIH0sXG4gICAgcGx1Z2luczogW1xuICAgICAgICB2dWUoKSxcbiAgICAgICAgQ29tcG9uZW50cyh7XG4gICAgICAgICAgICByZXNvbHZlcnM6IFtQcmltZVZ1ZVJlc29sdmVyKCldXG4gICAgICAgIH0pXG4gICAgXSxcbiAgICByZXNvbHZlOiB7XG4gICAgICAgIGFsaWFzOiB7XG4gICAgICAgICAgICAnQCc6IGZpbGVVUkxUb1BhdGgobmV3IFVSTCgnLi9zcmMnLCBpbXBvcnQubWV0YS51cmwpKVxuICAgICAgICB9XG4gICAgfSxcbiAgICBzZXJ2ZXI6IHtcbiAgICAgICAgcHJveHk6IHtcbiAgICAgICAgICAgICcvYXBpJzoge1xuICAgICAgICAgICAgICAgIHRhcmdldDogJ2h0dHBzOi8vYXBpLmtvbnR1cmFpLmt6JyxcbiAgICAgICAgICAgICAgICBjaGFuZ2VPcmlnaW46IHRydWUsXG4gICAgICAgICAgICAgICAgc2VjdXJlOiB0cnVlLFxuICAgICAgICAgICAgICAgIGNvbmZpZ3VyZTogKHByb3h5LCBfb3B0aW9ucykgPT4ge1xuICAgICAgICAgICAgICAgICAgICBwcm94eS5vbignZXJyb3InLCAoZXJyLCBfcmVxLCBfcmVzKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygncHJveHkgZXJyb3InLCBlcnIpO1xuICAgICAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICAgICAgcHJveHkub24oJ3Byb3h5UmVxJywgKHByb3h5UmVxLCByZXEsIF9yZXMpID0+IHtcbiAgICAgICAgICAgICAgICAgICAgICAgIGNvbnNvbGUubG9nKCdTZW5kaW5nIFJlcXVlc3QgdG8gdGhlIFRhcmdldDonLCByZXEubWV0aG9kLCByZXEudXJsKTtcbiAgICAgICAgICAgICAgICAgICAgfSk7XG4gICAgICAgICAgICAgICAgICAgIHByb3h5Lm9uKCdwcm94eVJlcycsIChwcm94eVJlcywgcmVxLCBfcmVzKSA9PiB7XG4gICAgICAgICAgICAgICAgICAgICAgICBjb25zb2xlLmxvZygnUmVjZWl2ZWQgUmVzcG9uc2UgZnJvbSB0aGUgVGFyZ2V0OicsIHByb3h5UmVzLnN0YXR1c0NvZGUsIHJlcS51cmwpO1xuICAgICAgICAgICAgICAgICAgICB9KTtcbiAgICAgICAgICAgICAgICB9XG4gICAgICAgICAgICB9XG4gICAgICAgIH1cbiAgICB9XG59KTtcbiJdLAogICJtYXBwaW5ncyI6ICI7QUFBdVMsU0FBUyxlQUFlLFdBQVc7QUFFMVUsU0FBUyx3QkFBd0I7QUFDakMsT0FBTyxTQUFTO0FBQ2hCLE9BQU8sZ0JBQWdCO0FBQ3ZCLFNBQVMsb0JBQW9CO0FBTDJKLElBQU0sMkNBQTJDO0FBUXpPLElBQU8sc0JBQVEsYUFBYTtBQUFBLEVBQ3hCLGNBQWM7QUFBQSxJQUNWLGFBQWE7QUFBQSxFQUNqQjtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ0wsSUFBSTtBQUFBLElBQ0osV0FBVztBQUFBLE1BQ1AsV0FBVyxDQUFDLGlCQUFpQixDQUFDO0FBQUEsSUFDbEMsQ0FBQztBQUFBLEVBQ0w7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNMLE9BQU87QUFBQSxNQUNILEtBQUssY0FBYyxJQUFJLElBQUksU0FBUyx3Q0FBZSxDQUFDO0FBQUEsSUFDeEQ7QUFBQSxFQUNKO0FBQUEsRUFDQSxRQUFRO0FBQUEsSUFDSixPQUFPO0FBQUEsTUFDSCxRQUFRO0FBQUEsUUFDSixRQUFRO0FBQUEsUUFDUixjQUFjO0FBQUEsUUFDZCxRQUFRO0FBQUEsUUFDUixXQUFXLENBQUMsT0FBTyxhQUFhO0FBQzVCLGdCQUFNLEdBQUcsU0FBUyxDQUFDLEtBQUssTUFBTSxTQUFTO0FBQ25DLG9CQUFRLElBQUksZUFBZSxHQUFHO0FBQUEsVUFDbEMsQ0FBQztBQUNELGdCQUFNLEdBQUcsWUFBWSxDQUFDLFVBQVUsS0FBSyxTQUFTO0FBQzFDLG9CQUFRLElBQUksa0NBQWtDLElBQUksUUFBUSxJQUFJLEdBQUc7QUFBQSxVQUNyRSxDQUFDO0FBQ0QsZ0JBQU0sR0FBRyxZQUFZLENBQUMsVUFBVSxLQUFLLFNBQVM7QUFDMUMsb0JBQVEsSUFBSSxzQ0FBc0MsU0FBUyxZQUFZLElBQUksR0FBRztBQUFBLFVBQ2xGLENBQUM7QUFBQSxRQUNMO0FBQUEsTUFDSjtBQUFBLElBQ0o7QUFBQSxFQUNKO0FBQ0osQ0FBQzsiLAogICJuYW1lcyI6IFtdCn0K