feat: implement targetologist dashboard module with competitor analysis and campaign management tools

This commit is contained in:
arys
2026-04-14 09:10:29 +05:00
parent 69d1dcd805
commit eb168fcb9d
14 changed files with 1113 additions and 804 deletions
+2 -1
View File
@@ -3,5 +3,6 @@
"allow": [ "allow": [
"Bash(npm run *)" "Bash(npm run *)"
] ]
} },
"$version": 3
} }
+7
View File
@@ -0,0 +1,7 @@
{
"permissions": {
"allow": [
"Bash(npm run *)"
]
}
}
@@ -430,8 +430,8 @@ export default {
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div class="kai-logo">KAI</div> <div class="kai-logo">KAI</div>
<div> <div>
<h1 class="kai-section-title">Создание ИИ-Брифа</h1> <h1 class="kai-section-title">Бриф маркетингового анализа</h1>
<p class="kai-section-sub">KAI Prime · Intelligent Onboarding</p> <p class="kai-section-sub">KAI Intelligence · Сбор данных для стратегии</p>
</div> </div>
</div> </div>
<Button icon="pi pi-times" text rounded size="small" class="text-surface-500" @click="$router.push('/marketing-analysis')" /> <Button icon="pi pi-times" text rounded size="small" class="text-surface-500" @click="$router.push('/marketing-analysis')" />
@@ -1,6 +1,6 @@
<script setup> <script setup>
import SectionWrapper from './SectionWrapper.vue';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import SectionWrapper from './SectionWrapper.vue';
const props = defineProps({ id: String, data: [Array, Object] }); const props = defineProps({ id: String, data: [Array, Object] });
@@ -26,61 +26,72 @@ const platColor = (p) => PLAT_COLORS[p] || '#808080';
const sorted = computed(() => [...normalizedData.value].sort((a, b) => b.followersVal - a.followersVal)); const sorted = computed(() => [...normalizedData.value].sort((a, b) => b.followersVal - a.followersVal));
// Scatter data // New Summary Metrics
const scatter = computed(() => normalizedData.value.map((c, i) => ({ const topFollowers = computed(() => sorted.value.length > 0 ? sorted.value[0] : null);
x: c.postsVal || 0, const topEr = computed(() => {
y: c.erVal || 0, if (!normalizedData.value.length) return null;
r: Math.max(Math.sqrt((c.followersVal || 0) / 1000) * 5, 4), return [...normalizedData.value].sort((a, b) => b.erVal - a.erVal)[0];
name: c.name, });
color: platColor(c.platform) const avgPosts = computed(() => {
}))); if (!normalizedData.value.length) return 0;
const maxX = computed(() => Math.max(Math.max(...scatter.value.map(d => d.x)) * 1.1, 10)); const sum = normalizedData.value.reduce((acc, c) => acc + c.postsVal, 0);
const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) * 1.2, 5)); return Math.round(sum / normalizedData.value.length);
});
const avgRating = computed(() => {
if (!normalizedData.value.length) return 0;
const sum = normalizedData.value.reduce((acc, c) => acc + c.ratingVal, 0);
return (sum / normalizedData.value.length).toFixed(1);
});
</script> </script>
<template> <template>
<SectionWrapper :id="id" title="Карта конкурентов" subtitle="Позиционирование игроков: Активность vs Вовлечённость" num="3"> <SectionWrapper :id="id" title="Анализ конкурентов" subtitle="Главные игроки на рынке и их показатели" num="3">
<!-- Scatter plot --> <!-- Summary Cards -->
<div class="competitor-plot-card"> <div class="summary-cards" v-if="normalizedData.length > 0">
<div class="plot-header"> <div class="summary-card">
<span class="plot-axis-label">Активность (посты/мес) </span> <div class="card-icon blue"><i class="pi pi-users" /></div>
<span class="plot-axis-label vertical">Вовлечённость (ER %) </span> <div class="card-info">
<i class="pi pi-info-circle kai-term-icon" v-tooltip.right="'Лидеры (справа вверху) делают много постов и получают высокий отклик.'"></i> <span class="card-label">Лидер рынка</span>
<span class="card-value">{{ topFollowers?.name || '—' }}</span>
<span class="card-desc">{{ fmtNum(topFollowers?.followersVal) }} подписчиков</span>
</div>
</div> </div>
<div class="plot-leader-zone">ЛИДЕРЫ</div> <div class="summary-card">
<div class="card-icon green"><i class="pi pi-heart-fill" /></div>
<div class="card-info">
<span class="card-label">Самый вовлекающий (ER)</span>
<span class="card-value">{{ topEr?.name || '—' }}</span>
<span class="card-desc" :style="{ color: topEr ? erColor(topEr.erVal) : '' }">{{ fmtPct(topEr?.erVal) }} ER</span>
</div>
</div>
<div class="plot-container"> <div class="summary-card">
<svg class="plot-svg" :viewBox="`0 0 500 220`" preserveAspectRatio="none"> <div class="card-icon yellow"><i class="pi pi-calendar" /></div>
<!-- Grid --> <div class="card-info">
<line x1="50" y1="10" x2="50" y2="200" class="plot-grid-line"/> <span class="card-label">Средняя активность</span>
<line x1="50" y1="200" x2="490" y2="200" class="plot-grid-line"/> <span class="card-value">{{ avgPosts }}</span>
<span class="card-desc">постов в месяц</span>
</div>
</div>
<!-- Leader quadrant --> <div class="summary-card">
<rect x="270" y="10" width="220" height="100" class="plot-leader-rect" rx="8"/> <div class="card-icon purple"><i class="pi pi-star-fill" /></div>
<div class="card-info">
<!-- Data points --> <span class="card-label">Средний рейтинг</span>
<g v-for="(pt, i) in scatter" :key="i" class="plot-point-group"> <span class="card-value text-yellow-500"> {{ avgRating }}</span>
<circle <span class="card-desc">по отзывам клиентов</span>
:cx="50 + (pt.x / maxX) * 440" </div>
: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> </div>
</div> </div>
<!-- Table --> <!-- Table -->
<div class="table-container"> <div class="table-container">
<div class="table-header">
<h4 class="table-title">Подробная таблица</h4>
<span class="table-count">{{ sorted.length }} конкурентов</span>
</div>
<table class="kai-table"> <table class="kai-table">
<thead> <thead>
<tr> <tr>
@@ -96,8 +107,13 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
</thead> </thead>
<tbody> <tbody>
<template v-for="(c, i) in sorted" :key="c.name"> <template v-for="(c, i) in sorted" :key="c.name">
<tr :class="{ 'top-row': i < 3 }" :style="i < 3 ? { '--top-color': ['#F59E0B','#939393','#CD7F32'][i] } : {}"> <tr :class="{ 'top-row': i < 3 }" :style="i < 3 ? { '--top-color': ['#F59E0B','#9CA3AF','#D97706'][i] } : {}">
<td class="row-num">{{ String(i+1).padStart(2,'0') }}</td> <td class="row-num">
<span v-if="i === 0" class="medal gold">🥇</span>
<span v-else-if="i === 1" class="medal silver">🥈</span>
<span v-else-if="i === 2" class="medal bronze">🥉</span>
<span v-else>{{ String(i+1).padStart(2,'0') }}</span>
</td>
<td> <td>
<div class="brand-cell"> <div class="brand-cell">
<div class="brand-avatar" :style="{ background: platColor(c.platform) + '20', color: platColor(c.platform), borderColor: platColor(c.platform) + '40' }"> <div class="brand-avatar" :style="{ background: platColor(c.platform) + '20', color: platColor(c.platform), borderColor: platColor(c.platform) + '40' }">
@@ -124,16 +140,28 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
<div class="expand-content"> <div class="expand-content">
<div class="sw-grid"> <div class="sw-grid">
<div class="sw-col"> <div class="sw-col">
<div class="sw-title green">Сильные стороны</div> <div class="sw-title green">
<i class="pi pi-check-circle"></i>
Сильные стороны
</div>
<ul class="sw-list"> <ul class="sw-list">
<li v-for="(s, si) in (c.strengths||[])" :key="si">{{ s }}</li> <li v-for="(s, si) in (c.strengths||[])" :key="si">
<span class="sw-icon green">+</span>
{{ s }}
</li>
<li v-if="!c.strengths?.length" class="empty-sw">Нет данных</li> <li v-if="!c.strengths?.length" class="empty-sw">Нет данных</li>
</ul> </ul>
</div> </div>
<div class="sw-col"> <div class="sw-col">
<div class="sw-title red">Слабые стороны</div> <div class="sw-title red">
<i class="pi pi-times-circle"></i>
Слабые стороны
</div>
<ul class="sw-list"> <ul class="sw-list">
<li v-for="(w, wi) in (c.weaknesses||[])" :key="wi">{{ w }}</li> <li v-for="(w, wi) in (c.weaknesses||[])" :key="wi">
<span class="sw-icon red"></span>
{{ w }}
</li>
<li v-if="!c.weaknesses?.length" class="empty-sw">Нет данных</li> <li v-if="!c.weaknesses?.length" class="empty-sw">Нет данных</li>
</ul> </ul>
</div> </div>
@@ -142,7 +170,12 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
</td> </td>
</tr> </tr>
</template> </template>
<tr v-if="!sorted.length"><td colspan="8" class="empty-table">Нет данных</td></tr> <tr v-if="!sorted.length"><td colspan="8" class="empty-table">
<div class="empty-state">
<i class="pi pi-search text-4xl mb-3"></i>
<p>Данные о конкурентах не найдены</p>
</div>
</td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
@@ -150,80 +183,78 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
</template> </template>
<style scoped> <style scoped>
.competitor-plot-card { /* ==================== SUMMARY CARDS ==================== */
.summary-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
gap: 16px;
margin-bottom: 32px;
}
.summary-card {
background: var(--kai-bg2); background: var(--kai-bg2);
border: 1px solid var(--kai-border); border: 1px solid var(--kai-border);
border-radius: 20px; border-radius: 16px;
padding: 24px; padding: 20px;
margin-bottom: 32px;
position: relative;
overflow: hidden;
}
.plot-header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 12px; gap: 16px;
margin-bottom: 24px; transition: transform 0.3s ease, box-shadow 0.3s ease;
} }
.plot-axis-label { .summary-card:hover {
font-size: 9.5px; transform: translateY(-2px);
font-weight: 800; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--kai-txt3);
} }
.plot-leader-zone { html[data-theme='light'] .summary-card:hover {
position: absolute; box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);
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 { .card-icon {
padding-left: 20px; width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
} }
.plot-grid-line { .card-icon.blue { background: rgba(45,123,255,0.1); color: #2D7BFF; }
stroke: var(--kai-border); .card-icon.green { background: rgba(16,185,129,0.1); color: #10B981; }
stroke-width: 1; .card-icon.yellow { background: rgba(245,158,11,0.1); color: #F59E0B; }
.card-icon.purple { background: rgba(139,92,246,0.1); color: #8B5CF6; }
.card-info {
display: flex;
flex-direction: column;
gap: 4px;
} }
.plot-leader-rect { .card-label {
fill: rgba(var(--kai-blue-rgb), 0.05); font-size: 11px;
}
.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; font-weight: 700;
fill: var(--kai-txt2); text-transform: uppercase;
text-anchor: middle; color: var(--kai-txt3);
pointer-events: none; letter-spacing: 0.05em;
} }
/* Table Stylings */ .card-value {
font-family: 'Unbounded', sans-serif;
font-size: 16px;
font-weight: 800;
color: var(--kai-txt);
line-height: 1.2;
}
.card-desc {
font-size: 11px;
color: var(--kai-txt2);
}
/* ==================== TABLE ==================== */
.table-container { .table-container {
border-radius: 16px; border-radius: 16px;
overflow: hidden; overflow: hidden;
@@ -231,6 +262,31 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
border: 1px solid var(--kai-border); border: 1px solid var(--kai-border);
} }
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 20px;
border-bottom: 1px solid var(--kai-border);
}
.table-title {
font-family: 'Unbounded', sans-serif;
font-size: 14px;
font-weight: 700;
color: var(--kai-txt);
margin: 0;
}
.table-count {
font-size: 11px;
color: var(--kai-txt3);
background: var(--kai-border);
padding: 4px 10px;
border-radius: 20px;
font-weight: 600;
}
.top-row { .top-row {
background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.03), transparent); background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.03), transparent);
border-left: 3px solid var(--top-color); border-left: 3px solid var(--top-color);
@@ -241,7 +297,12 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
font-size: 9px; font-size: 9px;
font-weight: 800; font-weight: 800;
color: var(--kai-txt3); color: var(--kai-txt3);
opacity: 0.6; text-align: center;
}
.medal {
font-size: 16px;
display: block;
} }
.brand-cell { .brand-cell {
@@ -304,6 +365,9 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
margin-bottom: 16px; margin-bottom: 16px;
display: flex;
align-items: center;
gap: 8px;
} }
.sw-title.green { color: var(--kai-green); } .sw-title.green { color: var(--kai-green); }
@@ -315,7 +379,7 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
margin: 0; margin: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 10px;
} }
.sw-list li { .sw-list li {
@@ -326,13 +390,16 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
gap: 10px; gap: 10px;
} }
.sw-list li::before { .sw-icon {
content: '→'; font-weight: 800;
color: var(--kai-txt3); font-size: 14px;
font-size: 10px; line-height: 1.4;
margin-top: 1px; flex-shrink: 0;
} }
.sw-icon.green { color: var(--kai-green); }
.sw-icon.red { color: var(--kai-red); }
.empty-sw { .empty-sw {
color: var(--kai-txt3) !important; color: var(--kai-txt3) !important;
font-style: italic; font-style: italic;
@@ -345,4 +412,36 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
padding: 40px; padding: 40px;
font-size: 12px; font-size: 12px;
} }
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
color: var(--kai-txt3);
}
.empty-state i {
opacity: 0.3;
}
.empty-state p {
margin: 0;
font-size: 12px;
}
/* ==================== RESPONSIVE ==================== */
@media (max-width: 768px) {
.summary-cards {
grid-template-columns: 1fr;
}
.sw-grid {
grid-template-columns: 1fr;
gap: 20px;
}
.expand-content {
padding: 16px;
}
}
</style> </style>
+6
View File
@@ -308,6 +308,12 @@ const router = createRouter({
component: () => import('@/views/pages/marketing/MarketingStrategyV3.vue'), component: () => import('@/views/pages/marketing/MarketingStrategyV3.vue'),
meta: { requiresAuth: true } meta: { requiresAuth: true }
}, },
{
path: '/marketing-analysis/ai-strategy',
name: 'marketing-ai-strategy',
component: () => import('@/views/pages/marketing/AIStrategyGeneratorPage.vue'),
meta: { requiresAuth: true }
},
{ {
path: '/marketing-analysis/v3/:id', path: '/marketing-analysis/v3/:id',
name: 'marketing-v3-analysis', name: 'marketing-v3-analysis',
+236 -243
View File
@@ -8,9 +8,10 @@ const navExternal = (url) => window.open(url, '_blank');
<template> <template>
<div class="mkt-hub"> <div class="mkt-hub">
<!-- AMBIENT GLOW --> <!-- AMBIENT GLOW (Premium Soft Mesh) -->
<div class="mkt-hub__glow1"></div> <div class="mkt-hub__glow1"></div>
<div class="mkt-hub__glow2"></div> <div class="mkt-hub__glow2"></div>
<div class="mkt-hub__glow3"></div>
<div class="mkt-hub__inner"> <div class="mkt-hub__inner">
@@ -21,7 +22,7 @@ const navExternal = (url) => window.open(url, '_blank');
KAI Intelligence · Платформа 2026 KAI Intelligence · Платформа 2026
</div> </div>
<h1 class="mkt-hub__title">Маркетинг-Хаб</h1> <h1 class="mkt-hub__title">Маркетинг-Хаб</h1>
<p class="mkt-hub__sub">Автономное управление маркетингом: от глубоких исследований рынка до генерации контента и запуска рекламных кампаний.</p> <p class="mkt-hub__sub">Исследования рынка, генерация стратегий и управление рекламными кампаниями всё в одном месте. Управляйте ростом вашего бизнеса легко и эффективно.</p>
</div> </div>
<!-- GRID --> <!-- GRID -->
@@ -48,14 +49,14 @@ const navExternal = (url) => window.open(url, '_blank');
</div> </div>
<!-- Сгенерировать стратегию --> <!-- Сгенерировать стратегию -->
<div class="mkt-card mkt-card--accent" @click="nav('/marketing-analysis/v3')"> <div class="mkt-card mkt-card--green" @click="nav('/marketing-analysis/v3/new')">
<div class="mkt-card__accent-glow"></div> <div class="mkt-card__glow"></div>
<div class="mkt-card__icon" style="background:rgba(0,212,139,0.15);color:#00D48B;"> <div class="mkt-card__icon" style="background:rgba(16,185,129,0.15);color:#10B981;">
<i class="pi pi-bolt"></i> <i class="pi pi-bolt"></i>
</div> </div>
<h3 class="mkt-card__title">Сгенерировать стратегию</h3> <h3 class="mkt-card__title">Сгенерировать стратегию</h3>
<p class="mkt-card__desc">Контент-фабрика. ИИ выберет соцсети, напишет посты и отрисует картинки на основе анализа</p> <p class="mkt-card__desc">Контент-фабрика. ИИ выберет соцсети, напишет посты и отрисует картинки на основе анализа</p>
<div class="mkt-card__tag">Требуется анализ</div> <div class="mkt-card__tag mkt-card__tag--green">Требуется анализ</div>
</div> </div>
<!-- Мои стратегии --> <!-- Мои стратегии -->
@@ -68,48 +69,32 @@ const navExternal = (url) => window.open(url, '_blank');
<div class="mkt-card__arrow"><i class="pi pi-arrow-right"></i></div> <div class="mkt-card__arrow"><i class="pi pi-arrow-right"></i></div>
</div> </div>
<!-- ИИ-Таргетолог --> <!-- ИИ-Таргетолог (внутренний) - Поменяли местами, теперь идет перед CRM -->
<div class="mkt-card mkt-card--blue" @click="navExternal('https://call-center.konturai.kz')"> <div class="mkt-card mkt-card--blue" @click="nav('/marketing-analysis/v3/targetologist/dashboard')">
<div class="mkt-card__glow"></div>
<div class="mkt-card__icon" style="background:rgba(45,123,255,0.15);color:#2D7BFF;"> <div class="mkt-card__icon" style="background:rgba(45,123,255,0.15);color:#2D7BFF;">
<i class="pi pi-bullseye"></i> <i class="pi pi-bullseye"></i>
</div> </div>
<h3 class="mkt-card__title">ИИ-Таргетолог</h3> <h3 class="mkt-card__title">ИИ-Таргетолог</h3>
<p class="mkt-card__desc">Развёртывание рекламных кампаний ИИ создаст аудитории, оптимизирует бюджет и запустит рекламу в Facebook, Instagram и TikTok</p> <p class="mkt-card__desc">Развёртывание рекламных кампаний ИИ создаст аудитории, оптимизирует бюджет и запустит рекламу в Facebook, Instagram и TikTok</p>
<div class="mkt-card__tag">Бета-режим API</div> <div class="mkt-card__tag mkt-card__tag--blue">Бета-режим API</div>
<div class="mkt-card__arrow"><i class="pi pi-arrow-right"></i></div>
</div> </div>
<!-- CRM --> <!-- CRM-система (внешняя ссылка) - Поменяли местами, теперь в конце -->
<div class="mkt-card mkt-card--teal" @click="nav('/crm')"> <div class="mkt-card mkt-card--teal" @click="navExternal('https://call-center.konturai.kz/')">
<div class="mkt-card__glow"></div>
<div class="mkt-card__icon" style="background:rgba(20,184,166,0.15);color:#14B8A6;"> <div class="mkt-card__icon" style="background:rgba(20,184,166,0.15);color:#14B8A6;">
<i class="pi pi-users"></i> <i class="pi pi-users"></i>
</div> </div>
<h3 class="mkt-card__title">CRM-система</h3> <h3 class="mkt-card__title">CRM-система</h3>
<p class="mkt-card__desc">Управление клиентской базой, сделками и воронкой продаж в едином пространстве</p> <p class="mkt-card__desc">Управление клиентской базой, сделками и воронкой продаж в едином пространстве</p>
<div class="mkt-card__arrow" style="background:rgba(20,184,166,0.1);color:#14B8A6;"><i class="pi pi-arrow-right"></i></div> <div class="mkt-card__external-badge">
</div> <i class="pi pi-external-link"></i>
Внешний сервис
</div>
<!-- AI TARGETOLOGIST full-width banner -->
<div class="mkt-banner" @click="nav('/marketing-analysis/v3/targetologist/dashboard')">
<div class="mkt-banner__glow"></div>
<div class="mkt-banner__left">
<div class="mkt-banner__icon">
<i class="pi pi-bullseye"></i>
</div>
<div>
<div class="mkt-banner__label">
<span class="mkt-banner__dot"></span>
Бета-режим API
</div>
<h2 class="mkt-banner__title">ИИ-Таргетолог</h2>
<p class="mkt-banner__desc">Развёртывание рекламных кампаний ИИ создаст аудитории, оптимизирует бюджет и запустит рекламу в Facebook, Instagram и TikTok</p>
</div> </div>
</div> </div>
<div class="mkt-banner__cta">
<span>Открыть кабинет</span>
<i class="pi pi-arrow-right"></i>
</div>
</div> </div>
</div> </div>
@@ -119,36 +104,54 @@ const navExternal = (url) => window.open(url, '_blank');
<style scoped> <style scoped>
/* ───── Page layout ───── */ /* ───── Page layout ───── */
.mkt-hub { .mkt-hub {
background: #F5F8FF; background: #F8FAFC; /* Clean, slightly cool white */
min-height: 100vh; min-height: 100vh;
font-family: 'Onest', sans-serif; font-family: 'Onest', -apple-system, BlinkMacSystemFont, sans-serif;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
padding: 48px 24px 80px; padding: 80px 32px 100px;
color: #0F172A; color: #0F172A;
} }
/* Ambient premium mesh background effects */
.mkt-hub__glow1 { .mkt-hub__glow1 {
position: absolute; position: absolute;
top: -120px; top: -150px;
right: -100px; left: -100px;
width: 600px; width: 600px;
height: 600px; height: 600px;
background: radial-gradient(circle, rgba(45,123,255,0.08) 0%, transparent 70%); background: radial-gradient(circle, rgba(45, 123, 255, 0.08) 0%, transparent 70%);
pointer-events: none; pointer-events: none;
animation: float-glow 15s ease-in-out infinite alternate;
} }
.mkt-hub__glow2 { .mkt-hub__glow2 {
position: absolute; position: absolute;
bottom: -80px; top: 20%;
left: -60px; right: -200px;
width: 400px; width: 800px;
height: 400px; height: 800px;
background: radial-gradient(circle, rgba(0,212,139,0.06) 0%, transparent 70%); background: radial-gradient(circle, rgba(168, 85, 247, 0.06) 0%, transparent 60%);
pointer-events: none; pointer-events: none;
animation: float-glow 20s ease-in-out infinite alternate-reverse;
}
.mkt-hub__glow3 {
position: absolute;
bottom: -100px;
left: 20%;
width: 700px;
height: 700px;
background: radial-gradient(circle, rgba(16, 185, 129, 0.05) 0%, transparent 70%);
pointer-events: none;
animation: float-glow 18s ease-in-out infinite alternate;
}
@keyframes float-glow {
0% { transform: translate(0, 0) scale(1); }
100% { transform: translate(40px, -40px) scale(1.1); }
} }
.mkt-hub__inner { .mkt-hub__inner {
max-width: 1100px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
position: relative; position: relative;
z-index: 1; z-index: 1;
@@ -157,52 +160,59 @@ const navExternal = (url) => window.open(url, '_blank');
/* ───── Header ───── */ /* ───── Header ───── */
.mkt-hub__header { .mkt-hub__header {
text-align: center; text-align: center;
margin-bottom: 52px; margin-bottom: 72px;
position: relative;
display: flex;
flex-direction: column;
align-items: center;
} }
.mkt-hub__badge { .mkt-hub__badge {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 8px;
padding: 4px 12px; padding: 6px 18px;
border-radius: 999px; border-radius: 999px;
border: 1px solid rgba(45,123,255,0.18); border: 1px solid rgba(45,123,255,0.2);
background: rgba(45,123,255,0.08); background: rgba(255, 255, 255, 0.8);
font-size: 9px; backdrop-filter: blur(10px);
font-weight: 800; font-size: 11px;
font-family: 'Unbounded', sans-serif; font-weight: 700;
font-family: 'Onest', sans-serif;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.12em;
color: #2D7BFF; color: #2D7BFF;
margin-bottom: 16px; margin-bottom: 24px;
box-shadow: 0 4px 12px rgba(45,123,255,0.05);
} }
.mkt-hub__badge-dot { .mkt-hub__badge-dot {
width: 5px; width: 6px;
height: 5px; height: 6px;
border-radius: 50%; border-radius: 50%;
background: #2D7BFF; background: #2D7BFF;
box-shadow: 0 0 8px #2D7BFF; box-shadow: 0 0 10px rgba(45,123,255,0.6);
animation: pulse-dot 2s infinite; animation: pulse-dot 2s ease-in-out infinite;
} }
@keyframes pulse-dot { @keyframes pulse-dot {
0%,100% { opacity:1; transform: scale(1); } 0%, 100% { opacity: 1; transform: scale(1); }
50% { opacity:0.5; transform: scale(0.8); } 50% { opacity: 0.5; transform: scale(0.85); }
} }
.mkt-hub__title { .mkt-hub__title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 32px; font-size: 52px;
font-weight: 900; font-weight: 800;
color: #0F172A; color: #0F172A;
letter-spacing: -0.03em; letter-spacing: -0.03em;
line-height: 1.1; line-height: 1.1;
margin-bottom: 12px; margin-bottom: 20px;
} }
.mkt-hub__sub { .mkt-hub__sub {
font-size: 13px; font-size: 16px;
color: #334155; color: #475569;
max-width: 500px; max-width: 620px;
margin: 0 auto; margin: 0 auto;
line-height: 1.6; line-height: 1.6;
font-weight: 400; font-weight: 400;
@@ -212,243 +222,226 @@ const navExternal = (url) => window.open(url, '_blank');
.mkt-hub__grid { .mkt-hub__grid {
display: grid; display: grid;
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
gap: 16px; gap: 24px;
margin-bottom: 16px; margin-bottom: 24px;
} }
@media (max-width: 900px) { @media (max-width: 1024px) {
.mkt-hub__grid { grid-template-columns: repeat(2, 1fr); } .mkt-hub__grid { grid-template-columns: repeat(2, 1fr); gap: 20px; }
} }
@media (max-width: 640px) { @media (max-width: 640px) {
.mkt-hub__grid { grid-template-columns: 1fr; } .mkt-hub__grid { grid-template-columns: 1fr; }
.mkt-hub { padding: 40px 20px 70px; }
.mkt-hub__title { font-size: 36px; }
.mkt-hub__header { margin-bottom: 40px; }
} }
/* ───── Cards ───── */ /* ───── Glassmorphism Cards ───── */
.mkt-card { .mkt-card {
background: #FFFFFF; background: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(45,123,255,0.18); border: 1px solid rgba(255, 255, 255, 1);
border-radius: 20px; backdrop-filter: blur(20px);
padding: 28px; border-radius: 28px;
padding: 36px;
cursor: pointer; cursor: pointer;
transition: all 0.3s cubic-bezier(0.4,0,0.2,1); transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 2px 8px rgba(45,123,255,0.05); box-shadow:
0 4px 6px rgba(0, 0, 0, 0.02),
0 12px 24px rgba(0, 0, 0, 0.03);
display: flex;
flex-direction: column;
min-height: 250px;
} }
/* Card hover overlay */
.mkt-card::before { .mkt-card::before {
content: ''; content: '';
position: absolute; position: absolute;
inset: 0; inset: 0;
opacity: 0; opacity: 0;
transition: opacity 0.3s; transition: opacity 0.4s;
border-radius: 20px; border-radius: 28px;
pointer-events: none; pointer-events: none;
z-index: 0;
}
.mkt-card::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4px;
background: linear-gradient(90deg, transparent, currentColor, transparent);
opacity: 0;
transition: opacity 0.4s;
} }
.mkt-card:hover { .mkt-card:hover {
transform: translateY(-4px); transform: translateY(-8px);
border-color: rgba(45,123,255,0.35); background: rgba(255, 255, 255, 0.95);
background: #F8FBFF; box-shadow:
box-shadow: 0 16px 36px rgba(15,23,42,0.1), 0 0 28px rgba(45,123,255,0.12); 0 24px 48px rgba(0, 0, 0, 0.06),
0 12px 20px rgba(0, 0, 0, 0.04);
} }
.mkt-card:hover::before { opacity: 1; } .mkt-card:hover::before { opacity: 1; }
.mkt-card:hover::after { opacity: 1; }
.mkt-card--blue::before { background: radial-gradient(circle at top right, rgba(45,123,255,0.1), transparent 60%); } /* Card color variants */
.mkt-card--purple::before { background: radial-gradient(circle at top right, rgba(168,85,247,0.1), transparent 60%); } .mkt-card--blue { color: #2D7BFF; }
.mkt-card--orange::before { background: radial-gradient(circle at top right, rgba(255,122,45,0.1), transparent 60%); } .mkt-card--blue::before { background: radial-gradient(circle at top right, rgba(45,123,255,0.06), transparent 70%); }
.mkt-card--accent::before { background: radial-gradient(circle at top right, rgba(0,212,139,0.1), transparent 60%); }
.mkt-card--teal::before { background: radial-gradient(circle at top right, rgba(20,184,166,0.12), transparent 60%); } .mkt-card--purple { color: #8B5CF6; }
.mkt-card--purple::before { background: radial-gradient(circle at top right, rgba(139,92,246,0.06), transparent 70%); }
.mkt-card--green { color: #10B981; }
.mkt-card--green::before { background: radial-gradient(circle at top right, rgba(16,185,129,0.06), transparent 70%); }
.mkt-card--orange { color: #F97316; }
.mkt-card--orange::before { background: radial-gradient(circle at top right, rgba(249,115,22,0.06), transparent 70%); }
.mkt-card--teal { color: #14B8A6; }
.mkt-card--teal::before { background: radial-gradient(circle at top right, rgba(20,184,166,0.06), transparent 70%); }
/* Card content */
.mkt-card > * { position: relative; z-index: 2; }
.mkt-card__glow {
position: absolute;
top: -40px;
right: -40px;
width: 160px;
height: 160px;
background: radial-gradient(circle, currentColor, transparent 70%);
opacity: 0.05;
pointer-events: none;
transition: opacity 0.4s, transform 0.4s;
z-index: 1;
}
.mkt-card:hover .mkt-card__glow {
opacity: 0.12;
transform: scale(1.3);
}
.mkt-card__icon { .mkt-card__icon {
width: 52px; width: 64px;
height: 52px; height: 64px;
border-radius: 14px; border-radius: 20px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 22px; font-size: 28px;
margin-bottom: 20px; margin-bottom: 28px;
transition: transform 0.3s; transition: transform 0.4s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 0 4px 12px rgba(0,0,0,0.04);
}
.mkt-card:hover .mkt-card__icon {
transform: scale(1.1) rotate(-5deg);
} }
.mkt-card:hover .mkt-card__icon { transform: scale(1.1) rotate(3deg); }
.mkt-card__title { .mkt-card__title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 14px; font-size: 20px;
font-weight: 700; font-weight: 700;
color: #0F172A; color: #0F172A;
margin-bottom: 8px; margin-bottom: 14px;
line-height: 1.25; line-height: 1.3;
letter-spacing: -0.01em;
} }
.mkt-card__desc { .mkt-card__desc {
font-size: 11.5px; font-size: 14px;
color: #334155; color: #475569;
line-height: 1.55; line-height: 1.7;
font-weight: 400; font-weight: 400;
} }
.mkt-card__arrow { .mkt-card__arrow {
position: absolute; position: absolute;
bottom: 24px; bottom: 32px;
right: 24px; right: 32px;
width: 32px; width: 40px;
height: 32px; height: 40px;
border-radius: 50%; border-radius: 50%;
background: rgba(45,123,255,0.1); background: rgba(0,0,0,0.03);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: var(--kai-blue); color: currentColor;
font-size: 13px; font-size: 16px;
opacity: 0; opacity: 0;
transform: translateX(-6px); transform: translateX(-12px);
transition: all 0.3s; transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
} }
.mkt-card:hover .mkt-card__arrow { .mkt-card:hover .mkt-card__arrow {
opacity: 1; opacity: 1;
transform: translateX(0); transform: translateX(0);
} background: currentColor;
color: #FFFFFF;
.mkt-card__accent-glow {
position: absolute;
top: -30px;
right: -30px;
width: 120px;
height: 120px;
background: radial-gradient(circle, rgba(0,212,139,0.15), transparent 70%);
pointer-events: none;
} }
.mkt-card__tag { .mkt-card__tag {
display: inline-block; display: inline-flex;
margin-top: 12px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(16,185,129,0.25);
background: rgba(16,185,129,0.07);
color: #10B981;
font-size: 8.5px;
font-weight: 800;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* ───── Targetologist Banner ───── */
.mkt-banner {
background: linear-gradient(135deg, rgba(45,123,255,0.08) 0%, rgba(139,92,246,0.05) 100%);
border: 1px solid rgba(45,123,255,0.18);
border-radius: 20px;
padding: 24px 32px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
cursor: pointer;
transition: all 0.3s;
position: relative;
overflow: hidden;
box-shadow: 0 4px 16px rgba(45,123,255,0.06);
}
.mkt-banner:hover {
border-color: rgba(45,123,255,0.6);
background: linear-gradient(135deg, rgba(45,123,255,0.18) 0%, rgba(99,51,220,0.12) 100%);
transform: translateY(-2px);
box-shadow: 0 24px 60px rgba(45,123,255,0.15);
}
.mkt-banner__glow {
position: absolute;
top: 0;
right: -60px;
width: 300px;
height: 200px;
background: radial-gradient(circle, rgba(45,123,255,0.15), transparent 70%);
pointer-events: none;
}
.mkt-banner__left {
display: flex;
align-items: center;
gap: 24px;
flex: 1;
min-width: 0;
}
.mkt-banner__icon {
width: 64px;
height: 64px;
border-radius: 18px;
background: rgba(45,123,255,0.15);
border: 1px solid rgba(45,123,255,0.3);
display: flex;
align-items: center;
justify-content: center;
font-size: 28px;
color: var(--kai-blue);
flex-shrink: 0;
transition: transform 0.3s;
}
.mkt-banner:hover .mkt-banner__icon { transform: scale(1.08) rotate(3deg); }
.mkt-banner__label {
display: flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: 9px; margin-top: auto;
font-weight: 800; padding: 6px 12px;
font-family: 'Unbounded', sans-serif; border-radius: 999px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.08em;
color: var(--kai-blue); align-self: flex-start;
margin-bottom: 4px;
} }
.mkt-banner__dot { .mkt-card__tag--green {
width: 5px; border: 1px solid rgba(16,185,129,0.3);
height: 5px; background: rgba(16,185,129,0.1);
border-radius: 50%; color: #10B981;
background: var(--kai-blue); }
animation: pulse-dot 2s infinite; .mkt-card__tag--blue {
border: 1px solid rgba(45,123,255,0.3);
background: rgba(45,123,255,0.1);
color: #2D7BFF;
} }
.mkt-banner__title { .mkt-card__external-badge {
font-family: 'Unbounded', sans-serif; display: inline-flex;
font-size: 20px;
font-weight: 900;
color: #0F172A;
margin-bottom: 6px;
line-height: 1.2;
}
.mkt-banner__desc {
font-size: 12px;
color: #334155;
line-height: 1.5;
max-width: 520px;
}
.mkt-banner__cta {
display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 10px 20px; margin-top: auto;
border-radius: 10px; padding: 8px 14px;
background: var(--kai-blue); border-radius: 12px;
color: #fff; background: rgba(255, 255, 255, 0.8);
font-size: 11.5px; border: 1px solid rgba(15, 23, 42, 0.1);
font-weight: 800; color: #475569;
font-family: 'Unbounded', sans-serif; font-size: 11px;
white-space: nowrap; font-weight: 600;
flex-shrink: 0; align-self: flex-start;
transition: all 0.3s; transition: all 0.3s;
} }
.mkt-banner:hover .mkt-banner__cta { .mkt-card:hover .mkt-card__external-badge {
background: #1a6bff; background: #0F172A;
box-shadow: 0 0 24px rgba(45,123,255,0.45); color: #FFFFFF;
transform: translateX(4px); border-color: #0F172A;
} }
@media (max-width: 768px) { /* Card entrance animation */
.mkt-banner { flex-direction: column; align-items: flex-start; } @keyframes card-enter {
.mkt-banner__cta { align-self: stretch; justify-content: center; } from {
.mkt-hub { padding: 32px 16px 60px; } opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
} }
.mkt-card {
animation: card-enter 0.6s cubic-bezier(0.16, 1, 0.3, 1) backwards;
}
.mkt-card:nth-child(1) { animation-delay: 0.05s; }
.mkt-card:nth-child(2) { animation-delay: 0.1s; }
.mkt-card:nth-child(3) { animation-delay: 0.15s; }
.mkt-card:nth-child(4) { animation-delay: 0.2s; }
.mkt-card:nth-child(5) { animation-delay: 0.25s; }
.mkt-card:nth-child(6) { animation-delay: 0.3s; }
</style> </style>
+271 -50
View File
@@ -783,17 +783,6 @@ onUnmounted(() => {
</div> </div>
</div> </div>
<div class="quick-actions-bar">
<Button label="Опубликовать в Facebook" icon="pi pi-facebook"
class="prime-action-btn fb"
:loading="isAutoPublishing"
@click="autoPublishFirstPost" />
<Button label="Запустить ИИ Таргет" icon="pi pi-bolt"
class="prime-action-btn target"
@click="openTargetingWizard" />
</div>
<!-- Weekly Sprints --> <!-- Weekly Sprints -->
<div v-if="strategyData.weeklyPlans?.length" class="sprints-section"> <div v-if="strategyData.weeklyPlans?.length" class="sprints-section">
<div class="section-title-row"> <div class="section-title-row">
@@ -902,31 +891,24 @@ onUnmounted(() => {
@click="handlePublishNow(post)" /> @click="handlePublishNow(post)" />
<div v-else class="card-status-label published"><i class="pi pi-check"></i> OK</div> <div v-else class="card-status-label published"><i class="pi pi-check"></i> OK</div>
</div> </div>
<button class="card-target-btn" @click="launchPostTarget(post)">
<i class="pi pi-bolt"></i> Запустить таргет
</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Actions Footer --> <!-- Bottom Action: Launch Targeting -->
<div class="results-footer-bar"> <div class="bottom-action-section">
<Button label="Сохранить как PDF" icon="pi pi-file-pdf" class="footer-btn pdf-btn" /> <div class="bottom-action-card">
<div class="bottom-action-icon">
<Button <i class="pi pi-bullseye"></i>
v-if="!isAutoPostingStarted" </div>
class="footer-btn main-run-btn" <div class="bottom-action-text">
@click="handleStartStrategy" <h4>Готовы запустить рекламу?</h4>
:loading="startingStrategy" <p>ИИ создаст аудитории, оптимизирует бюджет и запустит рекламу в Facebook, Instagram и TikTok</p>
> </div>
<i class="pi pi-rocket"></i> <Button label="Запустить ИИ Таргет" icon="pi pi-rocket"
<span>Активировать стратегию продвижения</span> class="bottom-action-btn"
</Button> @click="openTargetingWizard" />
<div v-else class="footer-success-status">
<i class="pi pi-check-circle"></i>
<span>Стратегия успешно активирована</span>
</div> </div>
</div> </div>
</div> </div>
@@ -1932,6 +1914,243 @@ onUnmounted(() => {
border-radius: 12px; border-radius: 12px;
} }
/* ───── WEEKLY SPRINTS (План реализации по неделям) ───── */
.sprints-section {
margin-top: 48px;
margin-bottom: 48px;
}
.sprints-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.sprint-item {
background: var(--kai-bg2);
border: 1px solid var(--kai-border);
border-radius: 16px;
overflow: hidden;
transition: all 0.3s ease;
}
.sprint-item:hover {
border-color: var(--kai-blue);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.2);
}
.sprint-item.is-active {
border-color: var(--kai-blue);
background: rgba(var(--kai-blue-rgb), 0.03);
}
.sprint-summary {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
cursor: pointer;
transition: background 0.2s ease;
}
.sprint-summary:hover {
background: rgba(var(--kai-blue-rgb), 0.05);
}
.week-num {
width: 40px;
height: 40px;
background: linear-gradient(135deg, var(--kai-blue), var(--kai-green));
color: #fff;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Unbounded', sans-serif;
font-size: 16px;
font-weight: 800;
flex-shrink: 0;
}
.week-info {
flex: 1;
min-width: 0;
}
.week-title {
font-family: 'Unbounded', sans-serif;
font-size: 14px;
font-weight: 700;
color: var(--kai-txt);
margin: 0 0 8px 0;
}
.week-tags {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.week-tag {
font-size: 11px;
font-weight: 600;
color: var(--kai-txt2);
background: rgba(var(--kai-blue-rgb), 0.08);
border: 1px solid rgba(var(--kai-blue-rgb), 0.15);
padding: 4px 10px;
border-radius: 6px;
white-space: nowrap;
transition: all 0.2s ease;
}
.week-tag:hover {
background: rgba(var(--kai-blue-rgb), 0.15);
border-color: var(--kai-blue);
color: var(--kai-blue);
}
.chevron {
font-size: 14px;
color: var(--kai-txt3);
transition: transform 0.3s ease;
flex-shrink: 0;
}
/* Expand animation */
.expand-enter-active,
.expand-leave-active {
transition: all 0.3s ease;
max-height: 500px;
overflow: hidden;
}
.expand-enter-from,
.expand-leave-to {
max-height: 0;
opacity: 0;
}
.sprint-details {
padding: 0 24px 24px;
border-top: 1px solid var(--kai-border);
}
.details-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 24px;
padding-top: 20px;
}
.detail-col label {
display: block;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--kai-txt3);
margin-bottom: 12px;
}
.themes-cloud {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.cloud-tag {
font-size: 12px;
font-weight: 600;
color: var(--kai-txt2);
background: rgba(var(--kai-green-rgb), 0.08);
border: 1px solid rgba(var(--kai-green-rgb), 0.15);
padding: 6px 12px;
border-radius: 8px;
transition: all 0.2s ease;
}
.cloud-tag:hover {
background: rgba(var(--kai-green-rgb), 0.15);
border-color: var(--kai-green);
color: var(--kai-green);
}
/* ───── BOTTOM ACTION (Запуск таргета) ───── */
.bottom-action-section {
margin-top: 64px;
margin-bottom: 32px;
}
.bottom-action-card {
background: linear-gradient(135deg, rgba(var(--kai-blue-rgb), 0.08) 0%, rgba(var(--kai-green-rgb), 0.08) 100%);
border: 1px solid rgba(var(--kai-blue-rgb), 0.2);
border-radius: 24px;
padding: 40px;
display: flex;
align-items: center;
gap: 32px;
transition: all 0.3s ease;
}
.bottom-action-card:hover {
border-color: var(--kai-blue);
box-shadow: 0 12px 40px rgba(var(--kai-blue-rgb), 0.15);
transform: translateY(-4px);
}
.bottom-action-icon {
width: 72px;
height: 72px;
background: linear-gradient(135deg, var(--kai-blue), var(--kai-green));
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
color: #fff;
flex-shrink: 0;
box-shadow: 0 8px 24px rgba(var(--kai-blue-rgb), 0.3);
}
.bottom-action-text {
flex: 1;
}
.bottom-action-text h4 {
font-family: 'Unbounded', sans-serif;
font-size: 18px;
font-weight: 700;
color: var(--kai-txt);
margin: 0 0 8px 0;
}
.bottom-action-text p {
font-size: 13px;
color: var(--kai-txt2);
line-height: 1.6;
margin: 0;
}
.bottom-action-btn {
font-size: 14px !important;
font-weight: 800 !important;
padding: 14px 32px !important;
border-radius: 16px !important;
background: linear-gradient(135deg, var(--kai-blue), var(--kai-green)) !important;
border: none !important;
color: #fff !important;
text-transform: uppercase;
letter-spacing: 0.05em;
box-shadow: 0 8px 24px rgba(var(--kai-blue-rgb), 0.3) !important;
transition: all 0.3s ease !important;
flex-shrink: 0;
}
.bottom-action-btn:hover {
transform: translateY(-2px) !important;
box-shadow: 0 12px 32px rgba(var(--kai-blue-rgb), 0.4) !important;
}
/* ───── POST CALENDAR ───── */ /* ───── POST CALENDAR ───── */
.calendar-section { margin-top: 48px; } .calendar-section { margin-top: 48px; }
.calendar-header { margin-bottom: 24px; border-bottom: 1px solid var(--kai-border); padding-bottom: 16px; } .calendar-header { margin-bottom: 24px; border-bottom: 1px solid var(--kai-border); padding-bottom: 16px; }
@@ -1962,13 +2181,23 @@ onUnmounted(() => {
.post-media { .post-media {
position: relative; position: relative;
aspect-ratio: 4/5; min-height: 200px;
background: #000; max-height: 400px;
background: var(--kai-bg3);
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
} }
.preview-obj { width: 100%; height: 100%; object-fit: cover; transition: transform 0.6s; } .preview-obj {
width: 100%;
height: 100%;
object-fit: contain;
transition: transform 0.6s;
background: transparent;
}
.post-card:hover .preview-obj { transform: scale(1.05); } .post-card:hover .preview-obj { transform: scale(1.05); }
.preview-overlay { .preview-overlay {
@@ -2042,21 +2271,6 @@ onUnmounted(() => {
.card-status-label { font-size: 9px; font-weight: 900; color: var(--kai-green); display: flex; align-items: center; gap: 4px; text-transform: uppercase; } .card-status-label { font-size: 9px; font-weight: 900; color: var(--kai-green); display: flex; align-items: center; gap: 4px; text-transform: uppercase; }
.card-target-btn {
width: 100%; margin-top: 12px; background: var(--kai-bg3); border: 1px solid var(--kai-border);
padding: 10px; border-radius: 12px; color: var(--kai-txt2); font-size: 10px; font-weight: 800;
text-transform: uppercase; display: flex; align-items: center; justify-content: center; gap: 8px;
transition: all 0.2s; cursor: pointer;
}
.card-target-btn:hover { background: var(--kai-blue); color: #fff; border-color: var(--kai-blue); }
/* ───── FOOTER ───── */
.results-footer-bar { margin-top: 48px; padding-top: 32px; border-top: 1px solid var(--kai-border); display: flex; gap: 16px; }
.footer-btn { border-radius: 18px !important; font-family: 'Unbounded', sans-serif !important; text-transform: uppercase; font-size: 11px !important; font-weight: 800 !important; padding: 16px 32px !important; }
.pdf-btn { background: var(--kai-bg2) !important; border: 1px solid var(--kai-border) !important; color: var(--kai-txt) !important; }
.main-run-btn { flex: 1; background: linear-gradient(135deg, var(--kai-green), #10b981) !important; box-shadow: 0 10px 30px rgba(16, 185, 129, 0.3); border: none !important; }
.footer-success-status { flex: 1; background: rgba(var(--kai-green-rgb), 0.1); border: 1px solid var(--kai-green); border-radius: 18px; display: flex; align-items: center; justify-content: center; gap: 12px; color: var(--kai-green); font-weight: 800; text-transform: uppercase; font-size: 11px; }
/* ───── MODAL ───── */ /* ───── MODAL ───── */
.premium-modal { .premium-modal {
:deep(.p-dialog-content) { background: transparent !important; } :deep(.p-dialog-content) { background: transparent !important; }
@@ -2084,7 +2298,14 @@ onUnmounted(() => {
@media (max-width: 768px) { @media (max-width: 768px) {
.strategy-page { padding: 16px; } .strategy-page { padding: 16px; }
.results-footer-bar { flex-direction: column; }
.posts-grid { grid-template-columns: 1fr; } .posts-grid { grid-template-columns: 1fr; }
.bottom-action-card {
flex-direction: column;
text-align: center;
padding: 32px 24px;
}
.bottom-action-btn {
width: 100% !important;
}
} }
</style> </style>
@@ -362,7 +362,7 @@ onMounted(() => {
{{ sm(statusRef).label }} {{ sm(statusRef).label }}
</div> </div>
</div> </div>
<div class="flex flex-wrap items-center gap-4 text-sm" style="color: var(--kai-txt2);"> <div class="flex flex-wrap items-center gap-4 text-sm" style="color: #64748B;">
<span><i class="pi pi-calendar mr-1"></i> {{ formatDateTime(campaign.createdAt) }}</span> <span><i class="pi pi-calendar mr-1"></i> {{ formatDateTime(campaign.createdAt) }}</span>
<span v-if="campaign.objective"><i class="pi pi-bullseye mr-1"></i> {{ objectiveLabel(campaign.objective) }}</span> <span v-if="campaign.objective"><i class="pi pi-bullseye mr-1"></i> {{ objectiveLabel(campaign.objective) }}</span>
<span class="flex items-center gap-2"> <span class="flex items-center gap-2">
@@ -381,37 +381,6 @@ onMounted(() => {
<i :class="publishingCampaign ? 'pi pi-spin pi-spinner' : 'pi pi-facebook'"></i> <i :class="publishingCampaign ? 'pi pi-spin pi-spinner' : 'pi pi-facebook'"></i>
Запустить стратегию Запустить стратегию
</button> </button>
<button
class="mkt-banner__cta"
style="border:none;"
:disabled="launchTargetDisabled"
:style="{ opacity: launchTargetDisabled ? 0.6 : 1 }"
@click="launchTarget"
>
<i :class="launchingTarget ? 'pi pi-spin pi-spinner' : (launchTargetDisabled ? 'pi pi-check' : 'pi pi-play')"></i>
{{ launchTargetLabel }}
</button>
<template v-if="statusRef === 'ACTIVE'">
<button class="wiz-btn-sec" style="padding: 10px 20px;" @click="pauseCampaign">
<i class="pi pi-pause"></i> Пауза
</button>
<button class="mkt-banner__cta" style="border:none;" @click="openInMeta">
Meta Ads <i class="pi pi-external-link"></i>
</button>
</template>
<button v-else-if="statusRef === 'PAUSED'" class="mkt-banner__cta" style="border:none; background: var(--kai-green);" @click="resumeCampaign">
<i class="pi pi-play"></i> Возобновить
</button>
<button v-else-if="statusRef === 'FAILED'" class="mkt-banner__cta" style="border:none; background: var(--kai-red);" @click="retryCampaign">
<i class="pi pi-refresh"></i> Повторить запуск
</button>
<button v-if="statusRef === 'ACTIVE'" class="wiz-btn-sec" style="padding: 10px 20px;" @click="syncInsights" :disabled="actionLoading">
<i :class="actionLoading ? 'pi pi-spin pi-spinner' : 'pi pi-sync'"></i>
</button>
</div> </div>
</div> </div>
@@ -427,8 +396,8 @@ onMounted(() => {
</div> </div>
<div v-else-if="statusRef === 'ACTIVE'" class="mkt-card" style="margin-bottom: 32px; padding: 12px 24px; border-color: rgba(0,212,139,0.3); background: rgba(0,212,139,0.05); display: flex; align-items: center; gap: 12px;"> <div v-else-if="statusRef === 'ACTIVE'" class="mkt-card" style="margin-bottom: 32px; padding: 12px 24px; border-color: rgba(0,212,139,0.3); background: rgba(0,212,139,0.05); display: flex; align-items: center; gap: 12px;">
<i class="pi pi-check-circle" style="color: var(--kai-green); font-size: 20px;"></i> <i class="pi pi-check-circle" style="color: #10B981; font-size: 20px;"></i>
<span style="font-weight: 700; color: var(--kai-green);">Кампания успешно запущена {{ formatDateTime(launchTimestamp) }}</span> <span style="font-weight: 700; color: #10B981;">Кампания успешно запущена {{ formatDateTime(launchTimestamp) }}</span>
</div> </div>
<!-- KPI GRID --> <!-- KPI GRID -->
@@ -453,21 +422,21 @@ onMounted(() => {
<!-- MAIN CONTENT --> <!-- MAIN CONTENT -->
<div class="mkt-card td-main-tabs" style="padding: 12px;"> <div class="mkt-card td-main-tabs" style="padding: 12px;">
<TabView v-model:activeIndex="activeTab"> <TabView v-model:activeIndex="activeTab" :lazy="true">
<TabPanel header="Метрики" value="0"> <TabPanel header="Метрики">
<CampaignMetrics :performance-metrics="campaign.performanceMetrics" :predicted-metrics="predictionMetrics" /> <CampaignMetrics v-if="activeTab === 0" :performance-metrics="campaign.performanceMetrics" :predicted-metrics="predictionMetrics" />
</TabPanel> </TabPanel>
<TabPanel header="ИИ Предсказание" value="1"> <TabPanel header="ИИ Предсказание">
<CampaignPrediction :campaign-id="campaignId" /> <CampaignPrediction v-if="activeTab === 1" :campaign-id="campaignId" />
</TabPanel> </TabPanel>
<TabPanel header="Инсайты" value="2"> <TabPanel header="Инсайты">
<CampaignInsights :campaign-id="campaignId" /> <CampaignInsights v-if="activeTab === 2" :campaign-id="campaignId" />
</TabPanel> </TabPanel>
<TabPanel header="Аудитория" value="3"> <TabPanel header="Аудитория">
<div class="grid gap-6 md:grid-cols-2 p-4"> <div v-if="activeTab === 3" class="grid gap-6 md:grid-cols-2 p-4">
<div class="mkt-card" style="background: rgba(45,123,255,0.02);"> <div class="mkt-card" style="background: rgba(45,123,255,0.02);">
<h3 class="wiz-label" style="opacity:0.8"><i class="pi pi-users"></i> Профиль аудитории</h3> <h3 class="wiz-label" style="opacity:0.8"><i class="pi pi-users"></i> Профиль аудитории</h3>
<div class="space-y-3 pt-4 text-sm" style="color: var(--kai-txt2);"> <div class="space-y-3 pt-4 text-sm" style="color: #64748B;">
<div><span class="info-label">Сегмент</span> {{ audienceProfile?.primarySegment || '—' }}</div> <div><span class="info-label">Сегмент</span> {{ audienceProfile?.primarySegment || '—' }}</div>
<div class="grid grid-cols-2 gap-4"> <div class="grid grid-cols-2 gap-4">
<div><span class="info-label">Возраст</span> {{ audienceProfile?.ageRange || '—' }}</div> <div><span class="info-label">Возраст</span> {{ audienceProfile?.ageRange || '—' }}</div>
@@ -479,7 +448,7 @@ onMounted(() => {
</div> </div>
<div class="mkt-card" style="background: rgba(45,123,255,0.02);"> <div class="mkt-card" style="background: rgba(45,123,255,0.02);">
<h3 class="wiz-label" style="opacity:0.8"><i class="pi pi-th-large"></i> Плейсменты и форматы</h3> <h3 class="wiz-label" style="opacity:0.8"><i class="pi pi-th-large"></i> Плейсменты и форматы</h3>
<div class="space-y-3 pt-4 text-sm" style="color: var(--kai-txt2);"> <div class="space-y-3 pt-4 text-sm" style="color: #64748B;">
<div><span class="info-label">Рекомендованные площадки</span> {{ formatArrayField(recommendation?.recommendedPlacements ?? null) }}</div> <div><span class="info-label">Рекомендованные площадки</span> {{ formatArrayField(recommendation?.recommendedPlacements ?? null) }}</div>
<div><span class="info-label">Форматы</span> {{ formatArrayField(recommendation?.recommendedAdFormats ?? null) }}</div> <div><span class="info-label">Форматы</span> {{ formatArrayField(recommendation?.recommendedAdFormats ?? null) }}</div>
<div><span class="info-label">Тип таргетинга</span> {{ targetingTypeLabel(recommendation?.recommendedType ?? null) }}</div> <div><span class="info-label">Тип таргетинга</span> {{ targetingTypeLabel(recommendation?.recommendedType ?? null) }}</div>
@@ -487,8 +456,8 @@ onMounted(() => {
</div> </div>
</div> </div>
</TabPanel> </TabPanel>
<TabPanel header="Структура" value="4"> <TabPanel header="Структура">
<CampaignAdSets :ad-sets="campaign.adSets" /> <CampaignAdSets v-if="activeTab === 4" :ad-sets="campaign.adSets" />
</TabPanel> </TabPanel>
</TabView> </TabView>
</div> </div>
@@ -98,10 +98,10 @@ const isError = ref(false);
const errorMessage = ref(''); const errorMessage = ref('');
const orchestratorPhases = [ const orchestratorPhases = [
{ code: 'ORCHESTRATING', label: 'ИИ рассчитывает сегменты таргета...', icon: 'pi pi-microchip-ai' }, { code: 'ORCHESTRATING', label: 'Анализ аудитории и создание сегментов', icon: 'pi pi-microchip-ai' },
{ code: 'MAPPING', label: 'Собираем группы объявлений по стратегии...', icon: 'pi pi-sitemap' }, { code: 'MAPPING', label: 'Формирование групп объявлений', icon: 'pi pi-sitemap' },
{ code: 'PUBLISHING', label: 'Публикуем в Meta/TikTok...', icon: 'pi pi-send' }, { code: 'PUBLISHING', label: 'Запуск рекламы в Facebook, Instagram и TikTok', icon: 'pi pi-send' },
{ code: 'ACTIVE', label: 'Готово: кампания запущена.', icon: 'pi pi-check-circle' } { code: 'ACTIVE', label: 'Кампания успешно запущена', icon: 'pi pi-check-circle' }
]; ];
const startGeneration = async () => { const startGeneration = async () => {
@@ -295,7 +295,7 @@ const togglePlat = (val) => {
<div class="wiz-check"><i class="pi pi-check" v-if="selectedPlatforms.includes('FB_IG')"></i></div> <div class="wiz-check"><i class="pi pi-check" v-if="selectedPlatforms.includes('FB_IG')"></i></div>
</div> </div>
<div class="wiz-platform" :class="{ 'wiz-platform--active': selectedPlatforms.includes('TIKTOK') }" @click="togglePlat('TIKTOK')"> <div class="wiz-platform" :class="{ 'wiz-platform--active': selectedPlatforms.includes('TIKTOK') }" @click="togglePlat('TIKTOK')">
<i class="pi pi-tiktok" style="color: #fff;"></i> <i class="pi pi-tiktok" style="color: #010101;"></i>
<span class="font-bold">TikTok</span> <span class="font-bold">TikTok</span>
<div class="wiz-check"><i class="pi pi-check" v-if="selectedPlatforms.includes('TIKTOK')"></i></div> <div class="wiz-check"><i class="pi pi-check" v-if="selectedPlatforms.includes('TIKTOK')"></i></div>
</div> </div>
@@ -332,8 +332,8 @@ const togglePlat = (val) => {
<div class="wiz-loading-orb"></div> <div class="wiz-loading-orb"></div>
<div class="text-center mb-10 relative z-10"> <div class="text-center mb-10 relative z-10">
<div class="wiz-loading-icon"><i class="pi pi-bolt"></i></div> <div class="wiz-loading-icon"><i class="pi pi-sparkles"></i></div>
<h2 class="wiz-loading-title">Запуск Машины</h2> <h2 class="wiz-loading-title">Запуск рекламной кампании</h2>
</div> </div>
<div v-if="isError" class="wiz-error"> <div v-if="isError" class="wiz-error">
@@ -1,10 +1,10 @@
<script setup> <script setup>
import { onMounted, ref } from 'vue';
import { useTargeting } from './composables/useTargeting';
import TargetingService from '@/service/TargetingService'; import TargetingService from '@/service/TargetingService';
import { useToast } from 'primevue/usetoast';
import Toast from 'primevue/toast'; import Toast from 'primevue/toast';
import { useToast } from 'primevue/usetoast';
import { onMounted, ref } from 'vue';
import OAuthConnectionWidget from './components/OAuthConnectionWidget.vue'; import OAuthConnectionWidget from './components/OAuthConnectionWidget.vue';
import { useTargeting } from './composables/useTargeting';
const { state, fetchCampaigns, connectAccount, formatCurrency } = useTargeting(); const { state, fetchCampaigns, connectAccount, formatCurrency } = useTargeting();
const toast = useToast(); const toast = useToast();
@@ -17,12 +17,12 @@ onMounted(async () => {
}); });
const STATUS_META = { const STATUS_META = {
ACTIVE: { color: '#10B981', bg: 'rgba(16,185,129,0.1)', border: 'rgba(16,185,129,0.28)', icon: 'pi pi-bolt', label: 'АКТИВНА', accent: '#10B981' }, ACTIVE: { color: '#10B981', bg: 'rgba(16,185,129,0.1)', border: 'rgba(16,185,129,0.25)', icon: 'pi pi-bolt', label: 'АКТИВНА', accent: '#10B981' },
PAUSED: { color: '#F59E0B', bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.28)', icon: 'pi pi-pause', label: 'НА ПАУЗЕ ', accent: '#F59E0B' }, PAUSED: { color: '#F59E0B', bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.25)', icon: 'pi pi-pause', label: 'НА ПАУЗЕ', accent: '#F59E0B' },
FAILED: { color: '#EF4444', bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.28)', icon: 'pi pi-exclamation-triangle', label: 'ОШИБКА', accent: '#EF4444' }, FAILED: { color: '#EF4444', bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.25)', icon: 'pi pi-exclamation-triangle', label: 'ОШИБКА', accent: '#EF4444' },
GENERATING: { color: '#2D7BFF', bg: 'rgba(45,123,255,0.1)', border: 'rgba(45,123,255,0.28)', icon: 'pi pi-spin pi-cog', label: 'ЗАПУСК', accent: '#2D7BFF' }, GENERATING: { color: '#2D7BFF', bg: 'rgba(45,123,255,0.1)', border: 'rgba(45,123,255,0.25)', icon: 'pi pi-spin pi-cog', label: 'ЗАПУСК', accent: '#2D7BFF' },
}; };
const sm = s => STATUS_META[(s||'').toUpperCase()] || { color: '#64748B', bg: 'rgba(100,116,139,0.08)', border: 'rgba(100,116,139,0.2)', icon: 'pi pi-circle', label: s || '—', accent: '#64748B' }; const sm = s => STATUS_META[(s||'').toUpperCase()] || { color: '#64748B', bg: 'rgba(100,116,139,0.08)', border: 'rgba(100,116,139,0.15)', icon: 'pi pi-circle', label: s || '—', accent: '#64748B' };
const totalBudget = () => campaigns.value.reduce((sum, c) => sum + (c.budgetKzt || 0), 0); const totalBudget = () => campaigns.value.reduce((sum, c) => sum + (c.budgetKzt || 0), 0);
const totalSpent = () => campaigns.value.reduce((sum, c) => sum + (c.spentKzt || 0), 0); const totalSpent = () => campaigns.value.reduce((sum, c) => sum + (c.spentKzt || 0), 0);
@@ -54,7 +54,7 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
<div class="td-page"> <div class="td-page">
<Toast /> <Toast />
<div class="td-glow td-glow--blue"></div> <div class="td-glow td-glow--blue"></div>
<div class="td-glow td-glow--purple"></div> <div class="td-glow td-glow--green"></div>
<div class="td-inner"> <div class="td-inner">
@@ -85,47 +85,47 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
<!-- KPI CARDS --> <!-- KPI CARDS -->
<div class="td-kpi-grid"> <div class="td-kpi-grid">
<div class="td-kpi-card"> <div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(45,123,255,0.12); color: #2D7BFF;"> <div class="td-kpi-card__icon" style="background: rgba(45,123,255,0.1); color: #2D7BFF;">
<i class="pi pi-list"></i> <i class="pi pi-list"></i>
</div> </div>
<div class="td-kpi-card__body"> <div class="td-kpi-card__body">
<div class="td-kpi-card__num">{{ campaigns.length }}</div> <div class="td-kpi-card__num">{{ campaigns.length }}</div>
<div class="td-kpi-card__label">Всего кампаний</div> <div class="td-kpi-card__label">Всего кампаний</div>
</div> </div>
<div class="td-kpi-card__accent" style="background: rgba(45,123,255,0.06);"></div> <div class="td-kpi-card__accent" style="background: rgba(45,123,255,0.05);"></div>
</div> </div>
<div class="td-kpi-card"> <div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(16,185,129,0.12); color: #10B981;"> <div class="td-kpi-card__icon" style="background: rgba(16,185,129,0.1); color: #10B981;">
<i class="pi pi-bolt"></i> <i class="pi pi-bolt"></i>
</div> </div>
<div class="td-kpi-card__body"> <div class="td-kpi-card__body">
<div class="td-kpi-card__num" style="color: #10B981;">{{ activeCnt() }}</div> <div class="td-kpi-card__num" style="color: #10B981;">{{ activeCnt() }}</div>
<div class="td-kpi-card__label">Активно сейчас</div> <div class="td-kpi-card__label">Активно сейчас</div>
</div> </div>
<div class="td-kpi-card__accent" style="background: rgba(16,185,129,0.06);"></div> <div class="td-kpi-card__accent" style="background: rgba(16,185,129,0.05);"></div>
</div> </div>
<div class="td-kpi-card"> <div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(249,115,22,0.12); color: #F97316;"> <div class="td-kpi-card__icon" style="background: rgba(249,115,22,0.1); color: #F97316;">
<i class="pi pi-dollar"></i> <i class="pi pi-dollar"></i>
</div> </div>
<div class="td-kpi-card__body"> <div class="td-kpi-card__body">
<div class="td-kpi-card__num">{{ formatCurrency(totalSpent()) }}</div> <div class="td-kpi-card__num">{{ formatCurrency(totalSpent()) }}</div>
<div class="td-kpi-card__label">Потрачено</div> <div class="td-kpi-card__label">Потрачено</div>
</div> </div>
<div class="td-kpi-card__accent" style="background: rgba(249,115,22,0.06);"></div> <div class="td-kpi-card__accent" style="background: rgba(249,115,22,0.05);"></div>
</div> </div>
<div class="td-kpi-card"> <div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(139,92,246,0.12); color: #8B5CF6;"> <div class="td-kpi-card__icon" style="background: rgba(139,92,246,0.1); color: #8B5CF6;">
<i class="pi pi-wallet"></i> <i class="pi pi-wallet"></i>
</div> </div>
<div class="td-kpi-card__body"> <div class="td-kpi-card__body">
<div class="td-kpi-card__num">{{ formatCurrency(totalBudget()) }}</div> <div class="td-kpi-card__num">{{ formatCurrency(totalBudget()) }}</div>
<div class="td-kpi-card__label">Общий лимит</div> <div class="td-kpi-card__label">Общий лимит</div>
</div> </div>
<div class="td-kpi-card__accent" style="background: rgba(139,92,246,0.06);"></div> <div class="td-kpi-card__accent" style="background: rgba(139,92,246,0.05);"></div>
</div> </div>
</div> </div>
@@ -298,92 +298,75 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
</template> </template>
<style scoped> <style scoped>
/* ──────────────────── PAGE ──────────────────── */ /* ──────────────────── PAGE (СВЕТЛАЯ ТЕМА, КАК В МАРКЕТИНГЕ) ──────────────────── */
.td-page { .td-page {
background: #F5F8FF; background: linear-gradient(180deg, #F8FAFF 0%, #F0F4FF 100%);
background-image:
radial-gradient(at 10% 0%, rgba(45,123,255,0.07) 0px, transparent 50%),
radial-gradient(at 90% 100%, rgba(139,92,246,0.05) 0px, transparent 50%);
min-height: 100vh; min-height: 100vh;
font-family: 'Onest', sans-serif; font-family: 'Onest', sans-serif;
padding: 40px 24px 100px; padding: 40px 24px 100px;
position: relative; position: relative;
overflow-x: hidden; overflow-x: hidden;
color: #0F172A;
} }
.td-glow { position: absolute; border-radius: 50%; pointer-events: none; z-index: 0; } .td-glow { position: absolute; border-radius: 50%; pointer-events: none; z-index: 0; }
.td-glow--blue { top: -180px; right: -120px; width: 600px; height: 600px; background: radial-gradient(circle, rgba(45,123,255,0.08) 0%, transparent 70%); } .td-glow--blue { top: -180px; right: -120px; width: 600px; height: 600px; background: radial-gradient(circle, rgba(45,123,255,0.08) 0%, transparent 70%); animation: float-glow 20s ease-in-out infinite; }
.td-glow--purple{ bottom: -100px; left: -80px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(139,92,246,0.06) 0%, transparent 70%); } .td-glow--green { bottom: -100px; left: -80px; width: 500px; height: 500px; background: radial-gradient(circle, rgba(16,185,129,0.06) 0%, transparent 70%); animation: float-glow 25s ease-in-out infinite reverse; }
@keyframes float-glow { 0%, 100% { transform: translate(0, 0) scale(1); } 50% { transform: translate(30px, -20px) scale(1.1); } }
.td-inner { max-width: 1100px; margin: 0 auto; position: relative; z-index: 1; } .td-inner { max-width: 1100px; margin: 0 auto; position: relative; z-index: 1; }
/* ──────────────────── HEADER ──────────────────── */ /* ──────────────────── HEADER ──────────────────── */
.td-header { margin-bottom: 40px; } .td-header { margin-bottom: 40px; }
.td-header__top { .td-header__top { display: flex; align-items: center; gap: 16px; margin-bottom: 20px; }
display: flex;
align-items: center;
gap: 16px;
margin-bottom: 20px;
}
.td-back-btn { .td-back-btn {
width: 40px; height: 40px; border-radius: 12px; width: 40px; height: 40px; border-radius: 12px;
border: 1px solid rgba(45,123,255,0.2); background: #fff; color: #64748B; border: 1px solid rgba(45,123,255,0.15); background: #fff; color: #64748B;
display: flex; align-items: center; justify-content: center; cursor: pointer; display: flex; align-items: center; justify-content: center; cursor: pointer;
transition: all 0.2s; box-shadow: 0 2px 8px rgba(45,123,255,0.06); flex-shrink: 0; transition: all 0.2s; box-shadow: 0 2px 8px rgba(45,123,255,0.05); flex-shrink: 0;
} }
.td-back-btn:hover { background: rgba(45,123,255,0.06); color: #2D7BFF; border-color: rgba(45,123,255,0.35); transform: translateX(-2px); } .td-back-btn:hover { background: rgba(45,123,255,0.06); color: #2D7BFF; border-color: rgba(45,123,255,0.3); transform: translateX(-2px); }
.td-badge { .td-badge {
display: inline-flex; align-items: center; gap: 7px; display: inline-flex; align-items: center; gap: 7px;
padding: 5px 14px; border-radius: 999px; padding: 5px 14px; border-radius: 999px;
border: 1px solid rgba(45,123,255,0.25); background: rgba(45,123,255,0.07); border: 1px solid rgba(45,123,255,0.2);
font-size: 10px; font-weight: 800; font-family: 'Unbounded', sans-serif; background: linear-gradient(135deg, rgba(45,123,255,0.08) 0%, rgba(45,123,255,0.04) 100%);
font-size: 10px; font-weight: 700; font-family: 'Onest', sans-serif;
text-transform: uppercase; letter-spacing: 0.08em; color: #2D7BFF; text-transform: uppercase; letter-spacing: 0.08em; color: #2D7BFF;
} }
.td-badge__dot { .td-badge__dot {
width: 6px; height: 6px; border-radius: 50%; background: #2D7BFF; width: 6px; height: 6px; border-radius: 50%; background: #2D7BFF;
box-shadow: 0 0 8px rgba(45,123,255,0.5); animation: td-blink 2s infinite; box-shadow: 0 0 8px rgba(45,123,255,0.5); animation: td-blink 2s infinite;
} }
@keyframes td-blink { 0%,100%{opacity:1}50%{opacity:0.4} } @keyframes td-blink { 0%,100%{opacity:1} 50%{opacity:0.4} }
.td-header__main { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; flex-wrap: wrap; }
.td-header__main {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 20px;
flex-wrap: wrap;
}
.td-title { .td-title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: clamp(22px, 3.5vw, 32px); font-size: clamp(22px, 3.5vw, 32px);
font-weight: 900; font-weight: 900;
color: #0F172A; color: #0F172A;
letter-spacing: -0.02em; letter-spacing: -0.02em;
line-height: 1.15; line-height: 1.15;
margin-bottom: 8px; margin: 0 0 8px 0;
}
.td-subtitle {
font-size: 14px;
color: #64748B;
line-height: 1.55;
max-width: 480px;
} }
.td-subtitle { font-size: 14px; color: #64748B; line-height: 1.55; max-width: 480px; margin: 0; }
.td-launch-btn { .td-launch-btn {
display: inline-flex; align-items: center; gap: 9px; display: inline-flex; align-items: center; gap: 9px;
padding: 13px 24px; border-radius: 14px; padding: 13px 24px; border-radius: 14px;
background: #2D7BFF; color: #fff; background: #2D7BFF; color: #fff;
font-size: 13px; font-weight: 700; font-family: 'Unbounded', sans-serif; font-size: 13px; font-weight: 700; font-family: 'Onest', sans-serif;
border: none; cursor: pointer; transition: all 0.3s; border: none; cursor: pointer; transition: all 0.3s;
box-shadow: 0 6px 24px rgba(45,123,255,0.35); white-space: nowrap; flex-shrink: 0; box-shadow: 0 6px 24px rgba(45,123,255,0.3); white-space: nowrap; flex-shrink: 0;
} }
.td-launch-btn:hover { background: #1a6bff; box-shadow: 0 10px 36px rgba(45,123,255,0.5); transform: translateY(-3px); } .td-launch-btn:hover { background: #1a6bff; box-shadow: 0 10px 36px rgba(45,123,255,0.4); transform: translateY(-3px); }
/* ──────────────────── KPI GRID ──────────────────── */ /* ──────────────────── KPI GRID ──────────────────── */
.td-kpi-grid { .td-kpi-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 40px; }
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 40px;
}
@media (max-width: 900px) { .td-kpi-grid { grid-template-columns: repeat(2, 1fr); } } @media (max-width: 900px) { .td-kpi-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .td-kpi-grid { grid-template-columns: 1fr; } } @media (max-width: 480px) { .td-kpi-grid { grid-template-columns: 1fr; } }
@@ -391,100 +374,73 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
background: #fff; background: #fff;
border: 1px solid rgba(45,123,255,0.12); border: 1px solid rgba(45,123,255,0.12);
border-radius: 18px; border-radius: 18px;
padding: 22px 20px; padding: 24px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: all 0.25s cubic-bezier(0.4,0,0.2,1); transition: all 0.25s cubic-bezier(0.4,0,0.2,1);
box-shadow: 0 2px 10px rgba(45,123,255,0.05); box-shadow: 0 4px 12px rgba(45,123,255,0.05);
}
.td-kpi-card:hover { transform: translateY(-4px); box-shadow: 0 16px 40px rgba(45,123,255,0.12); }
.td-kpi-card__accent {
position: absolute;
top: 0; right: 0;
width: 80px; height: 80px;
border-radius: 0 18px 0 80px;
} }
.td-kpi-card:hover { transform: translateY(-4px); box-shadow: 0 12px 32px rgba(45,123,255,0.1); border-color: rgba(45,123,255,0.25); }
.td-kpi-card__accent { position: absolute; top: 0; right: 0; width: 80px; height: 80px; border-radius: 0 18px 0 80px; }
.td-kpi-card__icon { .td-kpi-card__icon {
width: 44px; height: 44px; border-radius: 12px; width: 48px; height: 48px; border-radius: 14px;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center; font-size: 20px; margin-bottom: 16px;
font-size: 18px; margin-bottom: 16px; position: relative; z-index: 1; position: relative; z-index: 1; transition: transform 0.3s;
transition: transform 0.3s;
} }
.td-kpi-card:hover .td-kpi-card__icon { transform: scale(1.12) rotate(5deg); } .td-kpi-card:hover .td-kpi-card__icon { transform: scale(1.1); }
.td-kpi-card__body { position: relative; z-index: 1; } .td-kpi-card__body { position: relative; z-index: 1; }
.td-kpi-card__num { .td-kpi-card__num { font-family: 'Onest', sans-serif; font-size: 24px; font-weight: 900; color: #0F172A; line-height: 1; margin-bottom: 6px; }
font-family: 'Unbounded', sans-serif; .td-kpi-card__label { font-size: 10px; color: #94A3B8; font-weight: 700; text-transform: uppercase; letter-spacing: 0.07em; }
font-size: 22px; font-weight: 900; color: #0F172A; line-height: 1; margin-bottom: 6px;
}
.td-kpi-card__label {
font-size: 10px; color: #94A3B8; font-weight: 700;
text-transform: uppercase; letter-spacing: 0.07em;
}
/* ──────────────────── SECTION HEADER ──────────────────── */ /* ──────────────────── SECTION HEADER ──────────────────── */
.td-section-header { margin-bottom: 16px; } .td-section-header { margin-bottom: 16px; }
.td-section-label { .td-section-label {
display: flex; align-items: center; gap: 8px; display: flex; align-items: center; gap: 8px;
font-size: 11px; font-weight: 800; font-family: 'Unbounded', sans-serif; font-size: 11px; font-weight: 700; font-family: 'Onest', sans-serif;
text-transform: uppercase; letter-spacing: 0.1em; color: #94A3B8; text-transform: uppercase; letter-spacing: 0.1em; color: #64748B;
} }
.td-section-label i { font-size: 12px; } .td-section-label i { font-size: 12px; }
/* ──────────────────── INTEGRATIONS ──────────────────── */ /* ──────────────────── INTEGRATIONS ──────────────────── */
.td-integrations-grid { .td-integrations-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 40px; }
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
margin-bottom: 40px;
}
@media (max-width: 640px) { .td-integrations-grid { grid-template-columns: 1fr; } } @media (max-width: 640px) { .td-integrations-grid { grid-template-columns: 1fr; } }
/* ──────────────────── CAMPAIGNS HEADER ──────────────────── */ /* ──────────────────── CAMPAIGNS HEADER ──────────────────── */
.td-campaigns-header { .td-campaigns-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 16px; flex-wrap: wrap; }
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 16px;
flex-wrap: wrap;
}
.td-search { position: relative; } .td-search { position: relative; }
.td-search__icon { .td-search__icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: #94A3B8; font-size: 13px; pointer-events: none; }
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
color: #94A3B8; font-size: 13px; pointer-events: none;
}
.td-search__input { .td-search__input {
padding: 10px 38px 10px 42px; border-radius: 12px; padding: 10px 38px 10px 42px; border-radius: 12px;
background: #fff; border: 1px solid rgba(45,123,255,0.16); color: #0F172A; background: #fff; border: 1px solid rgba(45,123,255,0.15); color: #0F172A;
font-size: 13.5px; font-family: 'Onest', sans-serif; outline: none; font-size: 13.5px; font-family: 'Onest', sans-serif; outline: none;
width: 280px; transition: all 0.2s; box-shadow: 0 2px 8px rgba(45,123,255,0.05); width: 280px; transition: all 0.2s; box-shadow: 0 2px 8px rgba(45,123,255,0.04);
} }
.td-search__input:focus { border-color: rgba(45,123,255,0.4); width: 320px; box-shadow: 0 0 0 3px rgba(45,123,255,0.08); } .td-search__input:focus { border-color: #2D7BFF; width: 320px; box-shadow: 0 0 0 3px rgba(45,123,255,0.1); }
.td-search__input::placeholder { color: #94A3B8; } .td-search__input::placeholder { color: #94A3B8; }
.td-search__clear { .td-search__clear {
position: absolute; right: 10px; top: 50%; transform: translateY(-50%); position: absolute; right: 10px; top: 50%; transform: translateY(-50%);
width: 22px; height: 22px; border-radius: 50%; border: none; width: 22px; height: 22px; border-radius: 50%; border: none;
background: rgba(100,116,139,0.12); color: #64748B; background: rgba(100,116,139,0.1); color: #64748B;
display: flex; align-items: center; justify-content: center; display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 10px; transition: all 0.2s; cursor: pointer; font-size: 10px; transition: all 0.2s;
} }
.td-search__clear:hover { background: rgba(239,68,68,0.12); color: #EF4444; } .td-search__clear:hover { background: rgba(239,68,68,0.1); color: #EF4444; }
/* ──────────────────── SKELETON ──────────────────── */ /* ──────────────────── SKELETON ──────────────────── */
.td-skeleton-list { display: flex; flex-direction: column; gap: 12px; } .td-skeleton-list { display: flex; flex-direction: column; gap: 12px; }
.td-skeleton-item { .td-skeleton-item {
display: flex; background: #fff; display: flex; background: #fff; border: 1px solid rgba(45,123,255,0.1);
border: 1px solid rgba(45,123,255,0.1); border-radius: 18px; overflow: hidden; height: 130px; border-radius: 18px; overflow: hidden; height: 130px;
} }
.td-skeleton-stripe { width: 5px; flex-shrink: 0; background: rgba(45,123,255,0.1); } .td-skeleton-stripe { width: 5px; flex-shrink: 0; background: rgba(45,123,255,0.08); }
.td-skeleton-body { flex: 1; padding: 22px 24px; } .td-skeleton-body { flex: 1; padding: 22px 24px; }
.td-skeleton-line { height: 12px; border-radius: 6px; background: rgba(45,123,255,0.07); } .td-skeleton-line { height: 12px; border-radius: 6px; background: rgba(45,123,255,0.06); }
.td-skeleton-metrics { display: flex; gap: 24px; margin-top: 16px; } .td-skeleton-metrics { display: flex; gap: 24px; margin-top: 16px; }
.td-skeleton-metric { width: 80px; height: 40px; border-radius: 10px; background: rgba(45,123,255,0.07); } .td-skeleton-metric { width: 80px; height: 40px; border-radius: 10px; background: rgba(45,123,255,0.06); }
.shimmer { position: relative; overflow: hidden; } .shimmer { position: relative; overflow: hidden; }
.shimmer::after { .shimmer::after {
content: ''; position: absolute; inset: 0; content: ''; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(45,123,255,0.07), transparent); background: linear-gradient(90deg, transparent, rgba(45,123,255,0.06), transparent);
animation: shimmer 1.6s infinite; animation: shimmer 1.6s infinite;
} }
@keyframes shimmer { 0%{transform:translateX(-100%)} 100%{transform:translateX(100%)} } @keyframes shimmer { 0%{transform:translateX(-100%)} 100%{transform:translateX(100%)} }
@@ -492,100 +448,73 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
/* ──────────────────── EMPTY ──────────────────── */ /* ──────────────────── EMPTY ──────────────────── */
.td-empty { .td-empty {
text-align: center; padding: 80px 24px; background: #fff; text-align: center; padding: 80px 24px; background: #fff;
border: 1px solid rgba(45,123,255,0.14); border-radius: 24px; border: 1px solid rgba(45,123,255,0.12); border-radius: 24px;
box-shadow: 0 4px 24px rgba(45,123,255,0.06); box-shadow: 0 4px 24px rgba(45,123,255,0.06);
display: flex; flex-direction: column; align-items: center; gap: 16px; display: flex; flex-direction: column; align-items: center; gap: 16px;
} }
.td-empty__visual { position: relative; width: 84px; height: 84px; } .td-empty__visual { position: relative; width: 84px; height: 84px; }
.td-empty__ring { .td-empty__ring {
position: absolute; inset: -7px; border-radius: 50%; position: absolute; inset: -7px; border-radius: 50%;
border: 2px solid rgba(45,123,255,0.12); animation: spin-slow 8s linear infinite; border: 2px solid rgba(45,123,255,0.1); animation: spin-slow 8s linear infinite;
} }
@keyframes spin-slow { from{transform:rotate(0deg)} to{transform:rotate(360deg)} } @keyframes spin-slow { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
.td-empty__icon { .td-empty__icon {
width: 84px; height: 84px; border-radius: 22px; width: 84px; height: 84px; border-radius: 22px;
background: rgba(45,123,255,0.08); border: 1px solid rgba(45,123,255,0.18); background: rgba(45,123,255,0.08); border: 1px solid rgba(45,123,255,0.15);
display: flex; align-items: center; justify-content: center; font-size: 34px; color: #2D7BFF; display: flex; align-items: center; justify-content: center; font-size: 34px; color: #2D7BFF;
} }
.td-empty__title { font-family: 'Unbounded', sans-serif; font-size: 20px; font-weight: 800; color: #0F172A; } .td-empty__title { font-family: 'Onest', sans-serif; font-size: 20px; font-weight: 800; color: #0F172A; margin: 0; }
.td-empty__desc { font-size: 13.5px; color: #64748B; max-width: 420px; line-height: 1.65; } .td-empty__desc { font-size: 13.5px; color: #64748B; max-width: 420px; line-height: 1.65; margin: 0; }
/* ──────────────────── CAMPAIGN LIST ──────────────────── */ /* ──────────────────── CAMPAIGN LIST ──────────────────── */
.td-campaign-list { display: flex; flex-direction: column; gap: 12px; } .td-campaign-list { display: flex; flex-direction: column; gap: 12px; }
.td-campaign { .td-campaign {
display: flex; display: flex; background: #fff;
background: #fff;
border: 1px solid rgba(45,123,255,0.12); border: 1px solid rgba(45,123,255,0.12);
border-radius: 18px; border-radius: 18px; cursor: pointer; overflow: hidden;
cursor: pointer;
overflow: hidden;
transition: all 0.25s cubic-bezier(0.4,0,0.2,1); transition: all 0.25s cubic-bezier(0.4,0,0.2,1);
box-shadow: 0 2px 10px rgba(45,123,255,0.05); box-shadow: 0 2px 10px rgba(45,123,255,0.04);
} }
.td-campaign:hover { transform: translateY(-3px); box-shadow: 0 14px 40px rgba(45,123,255,0.12); border-color: rgba(45,123,255,0.28); } .td-campaign:hover { transform: translateY(-3px); box-shadow: 0 12px 32px rgba(45,123,255,0.1); border-color: rgba(45,123,255,0.25); }
.td-campaign__stripe { width: 5px; flex-shrink: 0; transition: width 0.2s; } .td-campaign__stripe { width: 5px; flex-shrink: 0; transition: width 0.2s; }
.td-campaign:hover .td-campaign__stripe { width: 6px; } .td-campaign:hover .td-campaign__stripe { width: 6px; }
.td-campaign__body { flex: 1; padding: 22px 24px; } .td-campaign__body { flex: 1; padding: 22px 24px; }
.td-campaign__top { margin-bottom: 18px; } .td-campaign__top { margin-bottom: 18px; }
.td-campaign__name-row { .td-campaign__name-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; }
display: flex; align-items: center; gap: 12px; flex-wrap: wrap; margin-bottom: 8px; .td-campaign__name { font-family: 'Onest', sans-serif; font-size: 15px; font-weight: 700; color: #0F172A; line-height: 1.3; flex: 1; margin: 0; }
}
.td-campaign__name {
font-family: 'Unbounded', sans-serif; font-size: 15px; font-weight: 700; color: #0F172A; line-height: 1.3; flex: 1;
}
.td-campaign__status { .td-campaign__status {
display: inline-flex; align-items: center; gap: 5px; display: inline-flex; align-items: center; gap: 5px;
padding: 4px 12px; border-radius: 999px; border: 1px solid; padding: 4px 12px; border-radius: 999px; border: 1px solid;
font-size: 9.5px; font-weight: 800; font-family: 'Unbounded', sans-serif; font-size: 9.5px; font-weight: 700; font-family: 'Onest', sans-serif;
text-transform: uppercase; letter-spacing: 0.07em; flex-shrink: 0; text-transform: uppercase; letter-spacing: 0.05em; flex-shrink: 0;
} }
.td-campaign__status i { font-size: 9px; } .td-campaign__status i { font-size: 9px; }
.td-campaign__meta { .td-campaign__meta { display: flex; align-items: center; gap: 10px; font-size: 12px; color: #64748B; flex-wrap: wrap; }
display: flex; align-items: center; gap: 10px; font-size: 12px; color: #94A3B8; flex-wrap: wrap;
}
.td-campaign__meta i { font-size: 11px; margin-right: 3px; } .td-campaign__meta i { font-size: 11px; margin-right: 3px; }
.td-plat-chips { display: flex; gap: 6px; flex-wrap: wrap; } .td-plat-chips { display: flex; gap: 6px; flex-wrap: wrap; }
.td-plat-chip { .td-plat-chip { display: inline-flex; align-items: center; gap: 4px; padding: 3px 9px; border-radius: 8px; font-size: 11px; font-weight: 600; }
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 9px; border-radius: 8px; font-size: 11px; font-weight: 600;
}
.td-plat-chip i { font-size: 10px; } .td-plat-chip i { font-size: 10px; }
.td-campaign__metrics { .td-campaign__metrics { display: flex; align-items: center; gap: 28px; flex-wrap: wrap; }
display: flex;
align-items: center;
gap: 28px;
flex-wrap: wrap;
}
.td-metric { display: flex; flex-direction: column; gap: 4px; min-width: 100px; } .td-metric { display: flex; flex-direction: column; gap: 4px; min-width: 100px; }
.td-metric__label { .td-metric__label { font-size: 9.5px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: #94A3B8; }
font-size: 9.5px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.08em; color: #94A3B8; .td-metric__val { font-family: 'Onest', sans-serif; font-size: 17px; font-weight: 900; color: #0F172A; line-height: 1; }
} .td-metric__bar { height: 4px; background: rgba(45,123,255,0.08); border-radius: 999px; overflow: hidden; margin-top: 4px; }
.td-metric__val { .td-metric__bar-fill { height: 100%; border-radius: 999px; transition: width 0.9s cubic-bezier(0.4,0,0.2,1); }
font-family: 'Unbounded', sans-serif; font-size: 17px; font-weight: 900; color: #0F172A; line-height: 1;
}
.td-metric__bar {
height: 4px; background: rgba(45,123,255,0.1); border-radius: 999px; overflow: hidden; margin-top: 4px;
}
.td-metric__bar-fill {
height: 100%; border-radius: 999px; transition: width 0.9s cubic-bezier(0.4,0,0.2,1);
}
.td-campaign__actions { display: flex; gap: 8px; align-items: center; margin-left: auto; } .td-campaign__actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
.td-act-btn { .td-act-btn {
width: 36px; height: 36px; border-radius: 10px; width: 36px; height: 36px; border-radius: 10px;
border: 1px solid rgba(45,123,255,0.14); background: rgba(45,123,255,0.04); border: 1px solid rgba(45,123,255,0.12); background: rgba(45,123,255,0.04);
color: #64748B; display: flex; align-items: center; justify-content: center; color: #64748B; display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s; font-size: 13px; cursor: pointer; transition: all 0.2s; font-size: 13px;
} }
.td-act-btn:hover { transform: scale(1.08); } .td-act-btn:hover { transform: scale(1.08); }
.td-act-btn--pause:hover { background: rgba(245,158,11,0.1); color: #F59E0B; border-color: rgba(245,158,11,0.3); } .td-act-btn--pause:hover { background: rgba(245,158,11,0.1); color: #F59E0B; border-color: rgba(245,158,11,0.25); }
.td-act-btn--play:hover { background: rgba(16,185,129,0.1); color: #10B981; border-color: rgba(16,185,129,0.3); } .td-act-btn--play:hover { background: rgba(16,185,129,0.1); color: #10B981; border-color: rgba(16,185,129,0.25); }
.td-act-btn--open:hover { background: rgba(45,123,255,0.1); color: #2D7BFF; border-color: rgba(45,123,255,0.3); } .td-act-btn--open:hover { background: rgba(45,123,255,0.1); color: #2D7BFF; border-color: rgba(45,123,255,0.25); }
</style> </style>
@@ -59,7 +59,7 @@ onMounted(() => { void loadInsights(); });
<div v-for="i in 4" :key="i" class="ci-skeleton"></div> <div v-for="i in 4" :key="i" class="ci-skeleton"></div>
</div> </div>
<div v-else-if="insights.length === 0" class="ci-empty"> <div v-else-if="!(insights?.length)" class="ci-empty">
<i class="pi pi-microchip-ai ci-empty__icon"></i> <i class="pi pi-microchip-ai ci-empty__icon"></i>
<p>ИИ анализирует кампанию. Первые инсайты появятся через 2448 часов после запуска.</p> <p>ИИ анализирует кампанию. Первые инсайты появятся через 2448 часов после запуска.</p>
<button class="ci-empty__btn" @click="syncInsights" :disabled="loading"> <button class="ci-empty__btn" @click="syncInsights" :disabled="loading">
@@ -70,7 +70,7 @@ onMounted(() => { void loadInsights(); });
<div v-else class="ci-grid"> <div v-else class="ci-grid">
<article <article
v-for="insight in insights" v-for="insight in (insights || []).filter(i => i)"
:key="insight.id" :key="insight.id"
class="ci-card" class="ci-card"
:style="{ background: insightMeta(insight.type).bg, borderColor: insightMeta(insight.type).border }" :style="{ background: insightMeta(insight.type).bg, borderColor: insightMeta(insight.type).border }"
@@ -108,16 +108,16 @@ onMounted(() => { void loadInsights(); });
header="Действие по инсайту" header="Действие по инсайту"
:style="{ width: '32rem' }" :style="{ width: '32rem' }"
:pt="{ :pt="{
root: { style: 'background:#0F1628;border:1px solid rgba(45,123,255,0.2);border-radius:20px;' }, root: { style: 'background:#FFFFFF;border:1px solid rgba(45,123,255,0.2);border-radius:24px;box-shadow: 0 10px 40px rgba(0,0,0,0.1);' },
header: { style: 'background:#0F1628;border-bottom:1px solid rgba(45,123,255,0.15);color:rgba(255,255,255,0.9);font-family:Unbounded,sans-serif;' }, header: { style: 'background:#FFFFFF;border-bottom:1px solid rgba(45,123,255,0.1);color:#0F172A;font-family:Onest,sans-serif;' },
content: { style: 'background:#0F1628;' } content: { style: 'background:#FFFFFF;padding:24px;' }
}" }"
> >
<div v-if="selectedInsight" class="ci-modal"> <div v-if="selectedInsight" class="ci-modal">
<h4 class="ci-modal__title">{{ selectedInsight.title }}</h4> <h4 class="ci-modal__title">{{ selectedInsight.title }}</h4>
<p class="ci-modal__desc">{{ selectedInsight.description }}</p> <p class="ci-modal__desc">{{ selectedInsight.description }}</p>
<div class="ci-modal__action"> <div class="ci-modal__action">
<i class="pi pi-check-circle" style="color:#00D48B; flex-shrink:0;"></i> <i class="pi pi-check-circle" style="color:#10B981; flex-shrink:0;"></i>
{{ selectedInsight.suggestedAction || 'Рекомендация будет добавлена позже.' }} {{ selectedInsight.suggestedAction || 'Рекомендация будет добавлена позже.' }}
</div> </div>
</div> </div>
@@ -146,11 +146,12 @@ onMounted(() => { void loadInsights(); });
display: flex; display: flex;
overflow: hidden; overflow: hidden;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
background: #fff;
} }
.ci-card:hover { .ci-card:hover {
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.3); box-shadow: 0 4px 20px rgba(45, 123, 255, 0.08);
} }
.ci-card__stripe { .ci-card__stripe {
@@ -176,10 +177,10 @@ onMounted(() => { void loadInsights(); });
} }
.ci-card__title { .ci-card__title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 13px; font-size: 14px;
font-weight: 700; font-weight: 700;
color: rgba(255, 255, 255, 0.92); color: #0F172A;
flex: 1; flex: 1;
line-height: 1.3; line-height: 1.3;
} }
@@ -187,7 +188,7 @@ onMounted(() => { void loadInsights(); });
.ci-card__type { .ci-card__type {
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 700;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
padding: 3px 10px; padding: 3px 10px;
@@ -198,7 +199,7 @@ onMounted(() => { void loadInsights(); });
.ci-card__desc { .ci-card__desc {
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.6); color: #64748B;
line-height: 1.6; line-height: 1.6;
margin-bottom: 14px; margin-bottom: 14px;
} }
@@ -207,10 +208,10 @@ onMounted(() => { void loadInsights(); });
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: 12px; font-size: 11px;
font-weight: 700; font-weight: 700;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
background: transparent; background: #fff;
border: 1px solid; border: 1px solid;
border-radius: 10px; border-radius: 10px;
padding: 6px 16px; padding: 6px 16px;
@@ -230,10 +231,10 @@ onMounted(() => { void loadInsights(); });
gap: 12px; gap: 12px;
padding: 48px 24px; padding: 48px 24px;
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.03); background: #fff;
border: 1px solid rgba(45, 123, 255, 0.12); border: 1px solid rgba(45, 123, 255, 0.12);
text-align: center; text-align: center;
color: rgba(255, 255, 255, 0.45); color: #64748B;
font-size: 13px; font-size: 13px;
line-height: 1.6; line-height: 1.6;
} }
@@ -250,11 +251,11 @@ onMounted(() => { void loadInsights(); });
gap: 8px; gap: 8px;
padding: 10px 16px; padding: 10px 16px;
border-radius: 12px; border-radius: 12px;
border: 1px solid rgba(45, 123, 255, 0.25); border: 1px solid rgba(45, 123, 255, 0.2);
background: rgba(45, 123, 255, 0.08); background: rgba(45, 123, 255, 0.05);
color: rgba(255, 255, 255, 0.85); color: #0F172A;
font-weight: 700; font-weight: 700;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
cursor: pointer; cursor: pointer;
transition: transform 0.2s, opacity 0.2s; transition: transform 0.2s, opacity 0.2s;
} }
@@ -266,46 +267,48 @@ onMounted(() => { void loadInsights(); });
.ci-empty__btn:not(:disabled):hover { .ci-empty__btn:not(:disabled):hover {
transform: translateY(-1px); transform: translateY(-1px);
background: rgba(45, 123, 255, 0.08);
} }
.ci-skeleton { .ci-skeleton {
height: 100px; height: 100px;
border-radius: 18px; border-radius: 18px;
background: rgba(255, 255, 255, 0.04); background: #fff;
border: 1px solid rgba(45, 123, 255, 0.1); border: 1px solid rgba(45, 123, 255, 0.08);
animation: ci-pulse 2s infinite; animation: ci-pulse 2s infinite;
} }
@keyframes ci-pulse { @keyframes ci-pulse {
0%, 100% { opacity: 0.6; } 0%, 100% { opacity: 0.8; }
50% { opacity: 0.3; } 50% { opacity: 0.4; }
} }
.ci-modal__title { .ci-modal__title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 16px; font-size: 18px;
font-weight: 700; font-weight: 800;
color: rgba(255, 255, 255, 0.92); color: #0F172A;
margin-bottom: 10px; margin-bottom: 12px;
} }
.ci-modal__desc { .ci-modal__desc {
font-size: 13px; font-size: 14px;
color: rgba(255, 255, 255, 0.6); color: #475569;
line-height: 1.6; line-height: 1.6;
margin-bottom: 16px; margin-bottom: 20px;
} }
.ci-modal__action { .ci-modal__action {
display: flex; display: flex;
align-items: flex-start; align-items: flex-start;
gap: 10px; gap: 12px;
padding: 14px; padding: 16px;
border-radius: 12px; border-radius: 16px;
background: rgba(0, 212, 139, 0.07); background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(0, 212, 139, 0.2); border: 1px solid rgba(16, 185, 129, 0.2);
font-size: 13px; font-size: 14px;
color: rgba(0, 212, 139, 0.9); color: #065F46;
line-height: 1.5; line-height: 1.5;
font-weight: 500;
} }
</style> </style>
@@ -129,11 +129,11 @@ const trendLabel = (trend: MetricTrend): string => {
gap: 10px; gap: 10px;
padding: 12px 20px; padding: 12px 20px;
border-radius: 14px; border-radius: 14px;
background: rgba(45, 123, 255, 0.07); background: rgba(45, 123, 255, 0.08);
border: 1px solid rgba(45, 123, 255, 0.2); border: 1px solid rgba(45, 123, 255, 0.2);
font-size: 13px; font-size: 13px;
font-weight: 500; font-weight: 600;
color: rgba(45, 123, 255, 0.9); color: #2D7BFF;
margin-bottom: 20px; margin-bottom: 20px;
} }
@@ -153,13 +153,14 @@ const trendLabel = (trend: MetricTrend): string => {
} }
.cm-card { .cm-card {
background: rgba(255, 255, 255, 0.04); background: #fff;
border: 1px solid rgba(45, 123, 255, 0.15); border: 1px solid rgba(45, 123, 255, 0.15);
border-radius: 18px; border-radius: 18px;
padding: 20px; padding: 20px;
transition: all 0.3s; transition: all 0.3s;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 12px rgba(45, 123, 255, 0.04);
} }
.cm-card::before { .cm-card::before {
@@ -167,15 +168,16 @@ const trendLabel = (trend: MetricTrend): string => {
position: absolute; position: absolute;
inset: 0; inset: 0;
opacity: 0; opacity: 0;
background: radial-gradient(circle at top right, rgba(45, 123, 255, 0.08), transparent 60%); background: radial-gradient(circle at top right, rgba(45, 123, 255, 0.06), transparent 60%);
pointer-events: none; pointer-events: none;
transition: opacity 0.3s; transition: opacity 0.3s;
} }
.cm-card:hover { .cm-card:hover {
border-color: rgba(45, 123, 255, 0.35); border-color: rgba(45, 123, 255, 0.3);
background: rgba(255, 255, 255, 0.06); background: #fff;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(45, 123, 255, 0.08);
} }
.cm-card:hover::before { .cm-card:hover::before {
@@ -192,10 +194,10 @@ const trendLabel = (trend: MetricTrend): string => {
.cm-card__name { .cm-card__name {
font-size: 11px; font-size: 11px;
font-weight: 700; font-weight: 700;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06em; letter-spacing: 0.06em;
color: rgba(255, 255, 255, 0.45); color: #64748B;
} }
.cm-card__trend { .cm-card__trend {
@@ -204,31 +206,33 @@ const trendLabel = (trend: MetricTrend): string => {
} }
.cm-card__val { .cm-card__val {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 24px; font-size: 24px;
font-weight: 900; font-weight: 900;
color: rgba(255, 255, 255, 0.92); color: #0F172A;
line-height: 1; line-height: 1;
margin-bottom: 6px; margin-bottom: 6px;
} }
.cm-card__val--empty { .cm-card__val--empty {
color: rgba(255, 255, 255, 0.25); color: #CBD5E1;
} }
.cm-card__range { .cm-card__range {
font-size: 11px; font-size: 11px;
color: rgba(255, 255, 255, 0.35); color: #94A3B8;
margin-bottom: 4px; margin-bottom: 4px;
font-weight: 500;
} }
.cm-card__helper { .cm-card__helper {
font-size: 11px; font-size: 11px;
color: #F59E0B; color: #F59E0B;
font-weight: 500;
} }
.cm-skel { .cm-skel {
background: rgba(255, 255, 255, 0.05); background: #F1F5F9;
border-radius: 8px; border-radius: 8px;
margin-bottom: 8px; margin-bottom: 8px;
animation: cm-pulse 2s infinite; animation: cm-pulse 2s infinite;
@@ -245,7 +249,7 @@ const trendLabel = (trend: MetricTrend): string => {
} }
@keyframes cm-pulse { @keyframes cm-pulse {
0%, 100% { opacity: 0.5; } 0%, 100% { opacity: 0.8; }
50% { opacity: 0.25; } 50% { opacity: 0.4; }
} }
</style> </style>
@@ -1,4 +1,4 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onMounted, ref, watch } from 'vue';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { ru } from 'date-fns/locale'; import { ru } from 'date-fns/locale';
@@ -107,23 +107,23 @@ const weeklyChartOptions = {
type: 'linear', type: 'linear',
position: 'left', position: 'left',
beginAtZero: true, beginAtZero: true,
ticks: { color: 'rgba(255,255,255,0.4)', font: { family: 'Onest' } }, ticks: { color: '#64748B', font: { family: 'Onest', size: 10 } },
grid: { color: 'rgba(255,255,255,0.06)' } grid: { color: 'rgba(45,123,255,0.06)' }
}, },
y1: { y1: {
type: 'linear', type: 'linear',
position: 'right', position: 'right',
beginAtZero: true, beginAtZero: true,
ticks: { color: 'rgba(255,255,255,0.4)', font: { family: 'Onest' } }, ticks: { color: '#64748B', font: { family: 'Onest', size: 10 } },
grid: { drawOnChartArea: false } grid: { drawOnChartArea: false }
}, },
x: { x: {
ticks: { color: 'rgba(255,255,255,0.4)', font: { family: 'Onest' } }, ticks: { color: '#64748B', font: { family: 'Onest', size: 10 } },
grid: { color: 'rgba(255,255,255,0.04)' } grid: { color: 'rgba(45,123,255,0.04)' }
} }
}, },
plugins: { plugins: {
legend: { labels: { color: 'rgba(255,255,255,0.6)', font: { family: 'Onest', size: 12 } } } legend: { labels: { color: '#0F172A', font: { family: 'Onest', size: 12, weight: '700' } } }
} }
}; };
@@ -164,7 +164,7 @@ const priorityMeta = (priority: string): { color: string; bg: string; border: st
const v = priority.toUpperCase(); const v = priority.toUpperCase();
if (v === 'HIGH') return { color: '#EF4444', bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.25)' }; if (v === 'HIGH') return { color: '#EF4444', bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.25)' };
if (v === 'MEDIUM') return { color: '#F59E0B', bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.25)' }; if (v === 'MEDIUM') return { color: '#F59E0B', bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.25)' };
return { color: 'rgba(255,255,255,0.4)', bg: 'rgba(255,255,255,0.04)', border: 'rgba(255,255,255,0.1)' }; return { color: '#64748B', bg: '#f1f5f9', border: '#e2e8f0' };
}; };
const marketSaturationMeta = computed(() => { const marketSaturationMeta = computed(() => {
@@ -281,7 +281,7 @@ onMounted(() => { void hydratePrediction(); });
<div class="cp-score-wrap"> <div class="cp-score-wrap">
<svg width="160" height="160" viewBox="0 0 190 190" class="cp-gauge"> <svg width="160" height="160" viewBox="0 0 190 190" class="cp-gauge">
<g transform="translate(95,95)"> <g transform="translate(95,95)">
<circle r="70" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="12" /> <circle r="70" fill="none" stroke="rgba(45,123,255,0.06)" stroke-width="12" />
<circle <circle
r="70" r="70"
fill="none" fill="none"
@@ -297,14 +297,14 @@ onMounted(() => { void hydratePrediction(); });
</svg> </svg>
<div class="cp-score-inner"> <div class="cp-score-inner">
<div class="cp-score-val" :style="{ color: scoreColor }">{{ score }}</div> <div class="cp-score-val" :style="{ color: scoreColor }">{{ score }}</div>
<div class="cp-score-sub">/100</div> <div class="cp-score-sub" style="color: #64748B;">/100</div>
</div> </div>
</div> </div>
<div class="cp-score-label" :style="{ color: scoreColor }">{{ predictionLabel }}</div> <div class="cp-score-label" :style="{ color: scoreColor }">{{ predictionLabel }}</div>
<div class="cp-score-conf">Уверенность: <b>{{ confidence }}%</b></div> <div class="cp-score-conf" style="color: #64748B;">Уверенность: <b style="color: #0F172A;">{{ confidence }}%</b></div>
<div class="cp-score-date">Обновлено: {{ formatDateTime(lastUpdated) }}</div> <div class="cp-score-date">Обновлено: {{ formatDateTime(lastUpdated) }}</div>
<button class="cp-btn-secondary mt-3" @click="refreshPrediction" :disabled="loading || generationRunning"> <button class="cp-btn-secondary mt-3" @click="refreshPrediction" :disabled="loading || generationRunning">
<i :class="generationRunning ? 'pi pi-spin pi-spinner' : 'pi pi-refresh'"></i> <i :class="generationRunning ? 'pi pi-spin pi-spinner' : 'pi pi-sync'"></i>
Обновить Обновить
</button> </button>
</div> </div>
@@ -327,7 +327,7 @@ onMounted(() => { void hydratePrediction(); });
<!-- WEEKLY CHART --> <!-- WEEKLY CHART -->
<div class="cp-card mb-4"> <div class="cp-card mb-4">
<div class="cp-card__label">Прогноз по неделям</div> <div class="cp-card__label">Прогноз по неделям</div>
<div class="cp-chart-wrap"> <div v-if="weeklyLabels.length" class="cp-chart-wrap">
<Chart type="bar" :data="weeklyChartData" :options="weeklyChartOptions" style="height:100%;" /> <Chart type="bar" :data="weeklyChartData" :options="weeklyChartOptions" style="height:100%;" />
</div> </div>
<div v-if="weeklyPhases.length" class="cp-phases-grid mt-4"> <div v-if="weeklyPhases.length" class="cp-phases-grid mt-4">
@@ -341,16 +341,16 @@ onMounted(() => { void hydratePrediction(); });
<!-- STRENGTHS & RISKS --> <!-- STRENGTHS & RISKS -->
<div class="cp-grid-2 mb-4"> <div class="cp-grid-2 mb-4">
<div class="cp-card cp-card--green"> <div class="cp-card cp-card--green">
<div class="cp-card__label" style="color:#00D48B;">Сильные стороны</div> <div class="cp-card__label" style="color:#059669;">Сильные стороны</div>
<ul class="cp-factor-list"> <ul class="cp-factor-list">
<li v-for="(factor, idx) in prediction?.strengthFactors ?? []" :key="`s-${idx}`"> <li v-for="(factor, idx) in prediction?.strengthFactors ?? []" :key="`s-${idx}`">
<i class="pi pi-check" style="color:#00D48B;"></i> {{ factor }} <i class="pi pi-check" style="color:#10B981;"></i> {{ factor }}
</li> </li>
<li v-if="!(prediction?.strengthFactors?.length)" class="cp-factor-empty"></li> <li v-if="!(prediction?.strengthFactors?.length)" class="cp-factor-empty"></li>
</ul> </ul>
</div> </div>
<div class="cp-card cp-card--amber"> <div class="cp-card cp-card--amber">
<div class="cp-card__label" style="color:#F59E0B;">Риски</div> <div class="cp-card__label" style="color:#D97706;">Риски</div>
<ul class="cp-factor-list"> <ul class="cp-factor-list">
<li v-for="(factor, idx) in prediction?.riskFactors ?? []" :key="`r-${idx}`"> <li v-for="(factor, idx) in prediction?.riskFactors ?? []" :key="`r-${idx}`">
<i class="pi pi-exclamation-triangle" style="color:#F59E0B;"></i> {{ factor }} <i class="pi pi-exclamation-triangle" style="color:#F59E0B;"></i> {{ factor }}
@@ -415,8 +415,8 @@ onMounted(() => { void hydratePrediction(); });
</div> </div>
</div> </div>
<div class="cp-cpl-reduction"> <div class="cp-cpl-reduction">
<i class="pi pi-arrow-down-right" style="color:#00D48B;"></i> <i class="pi pi-arrow-down-right" style="color:#059669;"></i>
Потенциальное снижение CPL: <b>{{ budgetOptimization.potentialCplReduction || '—' }}</b> Потенциальное снижение CPL: <b style="color: #059669;">{{ budgetOptimization.potentialCplReduction || '—' }}</b>
</div> </div>
</div> </div>
@@ -427,7 +427,7 @@ onMounted(() => { void hydratePrediction(); });
<div class="cp-comp-gauge"> <div class="cp-comp-gauge">
<svg width="144" height="144" viewBox="0 0 190 190"> <svg width="144" height="144" viewBox="0 0 190 190">
<g transform="translate(95,95)"> <g transform="translate(95,95)">
<circle r="70" fill="none" stroke="rgba(255,255,255,0.06)" stroke-width="12" /> <circle r="70" fill="none" stroke="rgba(45,123,255,0.06)" stroke-width="12" />
<circle <circle
r="70" r="70"
fill="none" fill="none"
@@ -446,12 +446,12 @@ onMounted(() => { void hydratePrediction(); });
<div class="cp-comp-info"> <div class="cp-comp-info">
<div class="cp-comp-saturation"> <div class="cp-comp-saturation">
Насыщенность рынка: Насыщенность рынка:
<span :style="{ color: marketSaturationMeta.color, fontWeight: '700' }">{{ marketSaturationMeta.label }}</span> <span :style="{ color: marketSaturationMeta.color, fontWeight: '800' }">{{ marketSaturationMeta.label }}</span>
</div> </div>
<div class="cp-card__label mt-3" style="margin-bottom:8px;">Конкурентные преимущества</div> <div class="cp-card__label mt-3" style="margin-bottom:8px;">Конкурентные преимущества</div>
<ul class="cp-factor-list"> <ul class="cp-factor-list">
<li v-for="(advantage, index) in competitiveAdvantages" :key="`adv-${index}`"> <li v-for="(advantage, index) in competitiveAdvantages" :key="`adv-${index}`">
<i class="pi pi-check" style="color:#2D7BFF;"></i> {{ advantage }} <i class="pi pi-check-circle" style="color:#2D7BFF;"></i> {{ advantage }}
</li> </li>
<li v-if="!competitiveAdvantages.length" class="cp-factor-empty"></li> <li v-if="!competitiveAdvantages.length" class="cp-factor-empty"></li>
</ul> </ul>
@@ -470,25 +470,26 @@ onMounted(() => { void hydratePrediction(); });
/* ─── Cards ─── */ /* ─── Cards ─── */
.cp-card { .cp-card {
background: rgba(255, 255, 255, 0.04); background: #fff;
border: 1px solid rgba(45, 123, 255, 0.15); border: 1px solid rgba(45, 123, 255, 0.15);
border-radius: 20px; border-radius: 20px;
padding: 24px; padding: 24px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
transition: border-color 0.3s; transition: all 0.3s;
box-shadow: 0 4px 12px rgba(45, 123, 255, 0.04);
} }
.cp-card--green { border-color: rgba(0, 212, 139, 0.2); background: rgba(0, 212, 139, 0.04); } .cp-card--green { border-color: rgba(16, 185, 129, 0.2); background: rgba(16, 185, 129, 0.04); }
.cp-card--amber { border-color: rgba(245, 158, 11, 0.2); background: rgba(245, 158, 11, 0.04); } .cp-card--amber { border-color: rgba(245, 158, 11, 0.2); background: rgba(245, 158, 11, 0.04); }
.cp-card__label { .cp-card__label {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 10px; font-size: 10px;
font-weight: 700; font-weight: 800;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.1em; letter-spacing: 0.1em;
color: rgba(255, 255, 255, 0.35); color: #64748B;
margin-bottom: 16px; margin-bottom: 16px;
} }
@@ -547,29 +548,29 @@ onMounted(() => { void hydratePrediction(); });
} }
.cp-score-val { .cp-score-val {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 36px; font-size: 42px;
font-weight: 900; font-weight: 900;
line-height: 1; line-height: 1;
} }
.cp-score-sub { .cp-score-sub {
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 0.35); font-weight: 700;
font-weight: 600;
} }
.cp-score-label { .cp-score-label {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 13px; font-size: 14px;
font-weight: 700; font-weight: 800;
margin-bottom: 4px; margin-bottom: 4px;
} }
.cp-score-conf, .cp-score-date { .cp-score-conf, .cp-score-date {
font-size: 12px; font-size: 11px;
color: rgba(255, 255, 255, 0.4);
margin-bottom: 2px; margin-bottom: 2px;
font-weight: 500;
color: #64748B;
} }
/* ─── Metrics ─── */ /* ─── Metrics ─── */
@@ -587,27 +588,27 @@ onMounted(() => { void hydratePrediction(); });
} }
.cp-metric-name { .cp-metric-name {
font-size: 12px; font-size: 13px;
font-weight: 600; font-weight: 700;
color: rgba(255, 255, 255, 0.65); color: #475569;
} }
.cp-metric-icon { .cp-metric-icon {
font-size: 12px; font-size: 12px;
margin-right: 8px; margin-right: 8px;
opacity: 0.85; color: #2D7BFF;
} }
.cp-metric-range { .cp-metric-range {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 11px; font-size: 12px;
font-weight: 700; font-weight: 800;
color: rgba(255, 255, 255, 0.85); color: #0F172A;
} }
:deep(.cp-progress .p-progressbar) { :deep(.cp-progress .p-progressbar) {
height: 6px !important; height: 6px !important;
background: rgba(255, 255, 255, 0.06) !important; background: #F1F5F9 !important;
border-radius: 999px !important; border-radius: 999px !important;
border: none !important; border: none !important;
} }
@@ -619,10 +620,11 @@ onMounted(() => { void hydratePrediction(); });
/* ─── Chart ─── */ /* ─── Chart ─── */
.cp-chart-wrap { .cp-chart-wrap {
height: 260px; height: 280px;
background: rgba(255, 255, 255, 0.02); background: rgba(45, 123, 255, 0.02);
border-radius: 14px; border-radius: 14px;
padding: 12px; padding: 16px;
border: 1px dashed rgba(45, 123, 255, 0.1);
} }
.cp-phases-grid { .cp-phases-grid {
@@ -637,7 +639,7 @@ onMounted(() => { void hydratePrediction(); });
.cp-phase-item { .cp-phase-item {
padding: 10px 14px; padding: 10px 14px;
background: rgba(255, 255, 255, 0.03); background: #fff;
border: 1px solid rgba(45, 123, 255, 0.12); border: 1px solid rgba(45, 123, 255, 0.12);
border-radius: 12px; border-radius: 12px;
} }
@@ -646,15 +648,16 @@ onMounted(() => { void hydratePrediction(); });
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.05em; letter-spacing: 0.05em;
color: rgba(255, 255, 255, 0.35); color: #94A3B8;
margin-bottom: 4px; margin-bottom: 4px;
font-weight: 700;
} }
.cp-phase-val { .cp-phase-val {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 13px; font-size: 13px;
font-weight: 700; font-weight: 800;
color: rgba(255, 255, 255, 0.85); color: #0F172A;
} }
/* ─── Factors ─── */ /* ─── Factors ─── */
@@ -672,13 +675,14 @@ onMounted(() => { void hydratePrediction(); });
align-items: flex-start; align-items: flex-start;
gap: 8px; gap: 8px;
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.65); color: #475569;
line-height: 1.5; line-height: 1.5;
font-weight: 500;
} }
.cp-factor-empty { .cp-factor-empty {
font-size: 12px; font-size: 12px;
color: rgba(255, 255, 255, 0.25); color: #94A3B8;
} }
/* ─── Recommendations ─── */ /* ─── Recommendations ─── */
@@ -690,14 +694,15 @@ onMounted(() => { void hydratePrediction(); });
.cp-rec-item { .cp-rec-item {
padding: 18px; padding: 18px;
background: rgba(255, 255, 255, 0.03); background: #fff;
border: 1px solid rgba(45, 123, 255, 0.12); border: 1px solid rgba(45, 123, 255, 0.12);
border-radius: 16px; border-radius: 16px;
transition: border-color 0.2s; transition: all 0.2s;
} }
.cp-rec-item:hover { .cp-rec-item:hover {
border-color: rgba(45, 123, 255, 0.3); border-color: rgba(45, 123, 255, 0.3);
box-shadow: 0 4px 15px rgba(45, 123, 255, 0.06);
} }
.cp-rec-badges { .cp-rec-badges {
@@ -709,8 +714,8 @@ onMounted(() => { void hydratePrediction(); });
.cp-rec-priority { .cp-rec-priority {
font-size: 9px; font-size: 9px;
font-weight: 700; font-weight: 800;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.08em; letter-spacing: 0.08em;
padding: 3px 10px; padding: 3px 10px;
@@ -723,27 +728,27 @@ onMounted(() => { void hydratePrediction(); });
align-items: center; align-items: center;
gap: 4px; gap: 4px;
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 700;
color: rgba(45, 123, 255, 0.8); color: #2D7BFF;
background: rgba(45, 123, 255, 0.1); background: rgba(45, 123, 255, 0.08);
border-radius: 999px; border-radius: 999px;
padding: 3px 10px; padding: 3px 10px;
} }
.cp-rec-title { .cp-rec-title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 13px; font-size: 15px;
font-weight: 700; font-weight: 800;
color: rgba(255, 255, 255, 0.9); color: #0F172A;
margin-bottom: 6px; margin-bottom: 8px;
line-height: 1.4; line-height: 1.4;
} }
.cp-rec-desc { .cp-rec-desc {
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.55); color: #64748B;
line-height: 1.6; line-height: 1.6;
margin-bottom: 8px; margin-bottom: 10px;
} }
.cp-rec-impact { .cp-rec-impact {
@@ -751,21 +756,22 @@ onMounted(() => { void hydratePrediction(); });
align-items: center; align-items: center;
gap: 6px; gap: 6px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 800;
color: #00D48B; color: #059669;
} }
/* ─── Info ─── */ /* ─── Info ─── */
.cp-info-text { .cp-info-text {
font-size: 13px; font-size: 14px;
color: rgba(255, 255, 255, 0.65); color: #475569;
line-height: 1.6; line-height: 1.6;
font-weight: 500;
} }
.cp-info-block { .cp-info-block {
padding: 14px; padding: 14px;
background: rgba(255, 255, 255, 0.02); background: rgba(248, 250, 252, 0.8);
border: 1px solid rgba(45, 123, 255, 0.1); border: 1px solid #E2E8F0;
border-radius: 12px; border-radius: 12px;
} }
@@ -773,15 +779,16 @@ onMounted(() => { void hydratePrediction(); });
font-size: 10px; font-size: 10px;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.06em; letter-spacing: 0.06em;
color: rgba(255, 255, 255, 0.35); color: #94A3B8;
margin-bottom: 6px; margin-bottom: 6px;
font-weight: 700; font-weight: 800;
} }
.cp-info-block__val { .cp-info-block__val {
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.75); color: #1E293B;
line-height: 1.5; line-height: 1.5;
font-weight: 600;
} }
.cp-cpl-reduction { .cp-cpl-reduction {
@@ -790,15 +797,79 @@ onMounted(() => { void hydratePrediction(); });
gap: 8px; gap: 8px;
margin-top: 12px; margin-top: 12px;
padding: 12px 16px; padding: 12px 16px;
background: rgba(0, 212, 139, 0.07); background: rgba(16, 185, 129, 0.08);
border: 1px solid rgba(0, 212, 139, 0.2); border: 1px solid rgba(16, 185, 129, 0.2);
border-radius: 12px; border-radius: 12px;
font-size: 13px; font-size: 13px;
color: rgba(255, 255, 255, 0.7); color: #065F46;
font-weight: 600;
} }
.cp-cpl-reduction b { /* ─── Competitive ─── */
color: #00D48B; .cp-comp-row {
display: flex;
align-items: center;
gap: 24px;
}
.cp-comp-gauge {
position: relative;
width: 144px;
height: 144px;
}
.cp-comp-gauge__val {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Onest', sans-serif;
font-size: 20px;
font-weight: 900;
color: #0F172A;
}
.cp-comp-saturation {
font-size: 14px;
color: #64748B;
margin-bottom: 8px;
}
/* ─── Misc ─── */
.cp-empty {
padding: 60px 40px;
text-align: center;
background: #fff;
border-radius: 24px;
border: 1px dashed rgba(45, 123, 255, 0.3);
}
.cp-empty__icon { font-size: 40px; color: #E2E8F0; margin-bottom: 20px; }
.cp-empty__title { font-family: 'Onest', sans-serif; font-size: 20px; font-weight: 900; color: #0F172A; margin-bottom: 8px; }
.cp-empty__sub { font-size: 14px; color: #94A3B8; margin-bottom: 24px; }
.cp-btn-primary {
display: inline-flex; align-items: center; gap: 10px;
padding: 12px 28px; border-radius: 12px; border: none;
background: #2D7BFF; color: #fff; font-weight: 800;
font-family: 'Onest', sans-serif; cursor: pointer; transition: all 0.2s;
}
.cp-btn-primary:hover { transform: translateY(-2px); box-shadow: 0 4px 15px rgba(45,123,255,0.4); }
.cp-btn-secondary {
display: inline-flex; align-items: center; gap: 8px;
padding: 10px 20px; border-radius: 10px; border: 1px solid #E2E8F0;
background: #fff; color: #475569; font-weight: 700;
font-family: 'Onest', sans-serif; font-size: 12px; cursor: pointer; transition: all 0.2s;
}
.cp-btn-secondary:hover { border-color: #2D7BFF; color: #2D7BFF; }
.cp-skel-block {
background: #F8FAFF !important;
animation: cm-pulse 2s infinite;
}
</style> color: #00D48B;
} }
/* ─── Competitive ─── */ /* ─── Competitive ─── */
@@ -154,8 +154,13 @@ const handleAccountSelection = async () => {
<style scoped> <style scoped>
.oauth-card { .oauth-card {
background: #fff;
border: 1px solid rgba(45,123,255,0.12);
border-radius: 24px;
padding: 32px; padding: 32px;
position: relative;
overflow: hidden; overflow: hidden;
box-shadow: 0 4px 12px rgba(45,123,255,0.05);
} }
.oauth-glow { .oauth-glow {
@@ -165,40 +170,41 @@ const handleAccountSelection = async () => {
width: 140px; width: 140px;
height: 140px; height: 140px;
filter: blur(40px); filter: blur(40px);
opacity: 0.3; opacity: 0.15;
transition: opacity 0.5s; transition: opacity 0.5s;
pointer-events: none; pointer-events: none;
} }
.group:hover .oauth-glow { opacity: 0.5; } .group:hover .oauth-glow { opacity: 0.25; }
.oauth-glow.blue-glow { background: var(--kai-blue); } .oauth-glow.blue { background: #2D7BFF; }
.oauth-glow.rose-glow { background: #ff2d55; } .oauth-glow.rose { background: #EF4444; }
.oauth-icon { .oauth-icon {
width: 64px; width: 64px;
height: 64px; height: 64px;
border-radius: 20px; border-radius: 20px;
background: rgba(255,255,255,0.03); background: #fff;
border: 1px solid var(--kai-border); border: 1px solid rgba(45,123,255,0.15);
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
font-size: 28px; font-size: 28px;
transition: all 0.3s; transition: all 0.3s;
box-shadow: 0 4px 10px rgba(0,0,0,0.03);
} }
.oauth-icon.blue-glow { color: #1877F2; } .oauth-icon.blue { color: #1877F2; }
.oauth-icon.rose-glow { color: #fff; } .oauth-icon.rose { color: #000; }
.oauth-title { .oauth-title {
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 20px; font-size: 20px;
font-weight: 800; font-weight: 800;
color: #fff; color: #0F172A;
margin-bottom: 4px; margin-bottom: 4px;
} }
.oauth-sub { .oauth-sub {
font-size: 13px; font-size: 13px;
color: var(--kai-txt3); color: #64748B;
font-weight: 500; font-weight: 500;
} }
@@ -207,33 +213,33 @@ const handleAccountSelection = async () => {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 10px 16px; padding: 10px 16px;
background: rgba(0,212,139,0.08); background: rgba(16,185,129,0.08);
border: 1px solid rgba(0,212,139,0.2); border: 1px solid rgba(16,185,129,0.2);
border-radius: 14px; border-radius: 14px;
color: var(--kai-green); color: #10B981;
} }
.oauth-connected i { font-size: 18px; } .oauth-connected i { font-size: 18px; }
.oauth-dialog :deep(.p-dialog-header) { .oauth-dialog :deep(.p-dialog-header) {
background: var(--kai-card); background: #fff;
color: #fff; color: #0F172A;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-size: 18px; font-size: 18px;
padding: 24px; padding: 24px;
} }
.oauth-dialog :deep(.p-dialog-content) { .oauth-dialog :deep(.p-dialog-content) {
background: var(--kai-card); background: #fff;
padding: 0 24px 24px; padding: 0 24px 24px;
} }
.oauth-dialog :deep(.p-dialog-footer) { .oauth-dialog :deep(.p-dialog-footer) {
background: var(--kai-card); background: #fff;
border-top: 1px solid var(--kai-border); border-top: 1px solid #f1f5f9;
padding: 16px 24px; padding: 16px 24px;
} }
.oauth-dialog-sub { .oauth-dialog-sub {
font-size: 13px; font-size: 13px;
color: var(--kai-txt3); color: #64748B;
margin-bottom: 20px; margin-bottom: 20px;
line-height: 1.5; line-height: 1.5;
} }
@@ -243,47 +249,47 @@ const handleAccountSelection = async () => {
align-items: center; align-items: center;
gap: 12px; gap: 12px;
padding: 14px; padding: 14px;
background: rgba(255,255,255,0.02); background: #f8fafc;
border: 1px solid var(--kai-border); border: 1px solid #e2e8f0;
border-radius: 14px; border-radius: 14px;
cursor: pointer; cursor: pointer;
transition: all 0.2s; transition: all 0.2s;
} }
.oauth-acc-item:hover { .oauth-acc-item:hover {
background: rgba(255,255,255,0.04); background: #f1f5f9;
border-color: var(--kai-border-hover); border-color: #cbd5e1;
} }
.oauth-acc-item--active { .oauth-acc-item--active {
background: rgba(45,123,255,0.08); background: rgba(45,123,255,0.08);
border-color: var(--kai-blue); border-color: #2D7BFF;
} }
.oauth-acc-id { .oauth-acc-id {
font-size: 10px; font-size: 10px;
background: rgba(255,255,255,0.05); background: rgba(0,0,0,0.05);
padding: 2px 6px; padding: 2px 6px;
border-radius: 4px; border-radius: 4px;
color: var(--kai-txt3); color: #64748B;
} }
/* Reuse from global/others */ /* Reuse from global/others */
.td-launch-btn { .td-launch-btn {
background: #fff; background: #f1f5f9;
color: #000; color: #0F172A;
font-family: 'Unbounded', sans-serif; font-family: 'Onest', sans-serif;
font-weight: 700; font-weight: 700;
font-size: 12px; font-size: 12px;
padding: 12px 24px; padding: 12px 24px;
border-radius: 12px; border-radius: 12px;
border: none; border: 1px solid #e2e8f0;
cursor: pointer; cursor: pointer;
transition: all 0.3s; transition: all 0.3s;
} }
.td-launch-btn:hover:not(:disabled) { .td-launch-btn:hover:not(:disabled) {
background: #e2e8f0;
transform: translateY(-2px); transform: translateY(-2px);
box-shadow: 0 0 20px rgba(255,255,255,0.2);
} }
.td-launch-btn:disabled { opacity: 0.5; cursor: not-allowed; } .td-launch-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.wiz-btn-pri { background: #fff; color: #000; border: none; font-family: 'Unbounded', sans-serif; font-weight: 700; border-radius: 8px; cursor: pointer; } .wiz-btn-pri { background: #2D7BFF; color: #fff; border: none; font-family: 'Onest', sans-serif; font-weight: 700; border-radius: 8px; cursor: pointer; }
.wiz-btn-sec { background: transparent; border: 1px solid var(--kai-border); color: var(--kai-txt2); font-weight: 600; border-radius: 8px; cursor: pointer; } .wiz-btn-sec { background: transparent; border: 1px solid #e2e8f0; color: #64748B; font-weight: 600; border-radius: 8px; cursor: pointer; }
</style> </style>