fix
This commit is contained in:
+65
-42
@@ -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>
|
||||
<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>
|
||||
<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>
|
||||
<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>Можно пройти за 5–7 минут.</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);">
|
||||
{{ b.label }}
|
||||
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="b.tip"></i>
|
||||
</span>
|
||||
<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>
|
||||
|
||||
<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="cii-value-display">
|
||||
<div class="cii-num" :style="{ color: ciiColor }">{{ cii }}</div>
|
||||
<div class="cii-label-text" :style="{ color: ciiColor }">{{ label }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="cii-description">
|
||||
<i class="pi pi-shield text-[10px] mr-2" :style="{ color: ciiColor }" />
|
||||
{{ ciiDesc }}
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- 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>
|
||||
<!-- 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 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 class="kai-progress-track"><div class="kai-progress-fill" :style="{ width: [35,25,20,20][i % 4]+'%', background: ciiColor }" /></div>
|
||||
<div v-if="!factors.length" class="empty-state">Нет данных</div>
|
||||
</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>
|
||||
|
||||
</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>
|
||||
|
||||
<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" 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" class="plot-leader-rect" rx="8"/>
|
||||
|
||||
<!-- Data points -->
|
||||
<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, 5)"
|
||||
:fill="pt.color"
|
||||
class="plot-point"
|
||||
/>
|
||||
<text
|
||||
:x="50 + (pt.x / maxX) * 440"
|
||||
:y="195 - (pt.y / maxY) * 185 - Math.max(pt.r, 5) - 6"
|
||||
class="plot-point-text"
|
||||
>{{ pt.name }}</text>
|
||||
</g>
|
||||
</svg>
|
||||
</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">
|
||||
<!-- 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"/>
|
||||
<!-- Leader quadrant -->
|
||||
<rect x="270" y="10" width="220" height="100" fill="rgba(45,123,255,0.03)" rx="4"/>
|
||||
<!-- Data points -->
|
||||
<g v-for="(pt, i) in scatter" :key="i">
|
||||
<circle
|
||||
:cx="50 + (pt.x / maxX) * 440"
|
||||
:cy="195 - (pt.y / maxY) * 185"
|
||||
:r="Math.max(pt.r, 6)"
|
||||
:fill="pt.color"
|
||||
fill-opacity="0.8"
|
||||
style="cursor:pointer;"
|
||||
/>
|
||||
<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>
|
||||
</g>
|
||||
</svg>
|
||||
</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>
|
||||
<th>★ Рейтинг</th>
|
||||
<th>Постов/мес <i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Среднее количество публикаций в месяц'"></i></th>
|
||||
<th></th>
|
||||
</tr></thead>
|
||||
<thead>
|
||||
<tr>
|
||||
<th width="40">#</th>
|
||||
<th>Бренд</th>
|
||||
<th>Платформа</th>
|
||||
<th>Подписчики</th>
|
||||
<th>ER</th>
|
||||
<th>★ Рейтинг</th>
|
||||
<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>
|
||||
</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>
|
||||
</ul>
|
||||
|
||||
<!-- 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 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>
|
||||
<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 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 class="d-card-info">
|
||||
<div class="d-card-label">
|
||||
Запросов / мес
|
||||
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="'Среднее количество поисковых запросов в месяц'"></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-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;">
|
||||
<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>
|
||||
</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>
|
||||
<!-- 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 -->
|
||||
<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>
|
||||
</g>
|
||||
</svg>
|
||||
<div v-else style="color:rgba(255,255,255,0.3);text-align:center;padding:40px;">Нет данных по сезонности</div>
|
||||
<!-- 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="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>
|
||||
|
||||
<!-- 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">Готовиться за 4–6 недель</div>
|
||||
</div>
|
||||
<!-- Grid -->
|
||||
<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>
|
||||
|
||||
<!-- 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="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 class="empty-chart">Данные по сезонности отсутствуют</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>
|
||||
<!-- 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 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-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>
|
||||
<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">
|
||||
<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>
|
||||
<!-- 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" class="gauge-track"/>
|
||||
<!-- Fill -->
|
||||
<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>
|
||||
<div class="gauge-status-badge" :style="{ background: gaugeColor(density) + '15', color: gaugeColor(density) }">
|
||||
{{ gaugeLabel(density) }}
|
||||
</div>
|
||||
<svg width="200" height="120" 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"/>
|
||||
<!-- 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>
|
||||
</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>
|
||||
|
||||
<!-- 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="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="flex items-center gap-3">
|
||||
<div class="kai-logo">KAI</div>
|
||||
<div class="kai-title-area">
|
||||
<div class="kai-title-niche">Market Intelligence · {{ exec?.geography || '' }}</div>
|
||||
<div class="kai-title-sub">Professional Ed. · KAI Prime (2026)</div>
|
||||
</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 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 class="reputation-value">
|
||||
{{ data.nicheReputationLevel }} <span class="reputation-total">/ 5.0</span>
|
||||
</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>
|
||||
<div style="font-size:28px;">🏆</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>
|
||||
</svg>
|
||||
<!-- 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>
|
||||
<tbody>
|
||||
<tr v-for="g in gapData" :key="g.label">
|
||||
<td style="font-weight:600;">
|
||||
{{ 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>
|
||||
<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>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<!-- 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 class="param-cell">
|
||||
{{ g.label }}
|
||||
<i class="pi pi-info-circle kai-term-icon" v-tooltip.top="g.tip"></i>
|
||||
</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="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>
|
||||
<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>
|
||||
</div>
|
||||
<small v-if="error" class="p-error">{{ error }}</small>
|
||||
<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>
|
||||
</div>
|
||||
<small v-if="error" class="p-error">{{ error }}</small>
|
||||
<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>
|
||||
|
||||
@@ -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,50 +100,214 @@ 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>
|
||||
<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 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>
|
||||
</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">
|
||||
<V4Executive id="s0" :data="exec" />
|
||||
<V4Landscape id="s1" :data="land" :demand="dem" />
|
||||
<V4Geo id="s2" :data="geo" />
|
||||
<V4Competitors id="s3" :data="comps" />
|
||||
<V4Content id="s4" :data="cont" />
|
||||
<V4Cii id="s5" :data="cii" />
|
||||
<V4Reputation id="s6" :data="rep" />
|
||||
<V4Behavioral id="s7" :data="beh" />
|
||||
<V4Demand id="s8" :data="dem" />
|
||||
<V4Positioning id="s9" :data="pos" />
|
||||
<V4Conclusions id="s10" :data="concl" />
|
||||
<V4Rationale id="s11" :data="rat" @strategy="goStrategy" />
|
||||
<div class="dashboard-grid">
|
||||
<V4Executive id="s0" :data="exec" />
|
||||
<V4Landscape id="s1" :data="land" :demand="dem" />
|
||||
<V4Geo id="s2" :data="geo" />
|
||||
<V4Competitors id="s3" :data="comps" />
|
||||
<V4Content id="s4" :data="cont" />
|
||||
<V4Cii id="s5" :data="cii" />
|
||||
<V4Reputation id="s6" :data="rep" />
|
||||
<V4Behavioral id="s7" :data="beh" />
|
||||
<V4Demand id="s8" :data="dem" />
|
||||
<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>
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user