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