This commit is contained in:
arys
2026-04-10 12:50:08 +05:00
parent 5432d679fa
commit e7eae6a877
21 changed files with 2117 additions and 1027 deletions
+41 -37
View File
@@ -1,16 +1,16 @@
/* =========================================================
KAI v4.0 Dashboard — Dark Theme Globals
KAI v4.0 Dashboard - Light Theme Globals
Loaded by MarketingAnalysisV3.vue via <style>
========================================================= */
@import url('https://fonts.googleapis.com/css2?family=Unbounded:wght@400;700;900&family=Onest:wght@400;500;600;700&display=swap');
:root {
--kai-bg: #07090F;
--kai-bg2: #0A0D14;
--kai-card: rgba(255,255,255,0.03);
--kai-card-hover: rgba(255,255,255,0.06);
--kai-border: rgba(45,123,255,0.12);
--kai-border-hover: rgba(45,123,255,0.3);
--kai-bg: #F5F8FF;
--kai-bg2: #EEF4FF;
--kai-card: #FFFFFF;
--kai-card-hover: #F8FBFF;
--kai-border: rgba(45,123,255,0.18);
--kai-border-hover: rgba(45,123,255,0.35);
--kai-blue: #2D7BFF;
--kai-blue-rgb: 45, 123, 255;
--kai-green: #10B981;
@@ -19,10 +19,10 @@
--kai-purple: #8B5CF6;
--kai-yellow: #F59E0B;
--kai-red: #EF4444;
--kai-txt: rgba(255,255,255,0.95);
--kai-txt2: rgba(255,255,255,0.65);
--kai-txt3: rgba(255,255,255,0.40);
--kai-txt: #0F172A;
--kai-txt2: #334155;
--kai-txt3: #64748B;
/* Font size unification - smaller & professional */
--kai-fs-base: 12.5px;
--kai-fs-sm: 11px;
@@ -35,9 +35,9 @@
/* Layout */
.kai-page {
background: var(--kai-bg);
background-image:
radial-gradient(at 0% 0%, rgba(var(--kai-blue-rgb), 0.05) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(var(--kai-green-rgb), 0.03) 0px, transparent 50%);
background-image:
radial-gradient(at 0% 0%, rgba(var(--kai-blue-rgb), 0.09) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(var(--kai-green-rgb), 0.06) 0px, transparent 50%);
min-height: 100vh;
font-family: 'Onest', sans-serif;
color: var(--kai-txt);
@@ -51,9 +51,9 @@
top: 0;
z-index: 100;
height: 64px;
background: rgba(10,14,26,0.96);
background: rgba(255,255,255,0.92);
backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(45,123,255,0.2);
border-bottom: 1px solid rgba(45,123,255,0.16);
display: flex;
align-items: center;
padding: 0 32px;
@@ -140,7 +140,7 @@
white-space: nowrap;
}
.kai-pdf-btn:hover {
background: var(--kai-card);
background: #F0F7FF;
border-color: var(--kai-border-hover);
color: var(--kai-txt);
}
@@ -153,6 +153,7 @@
border-radius: 16px;
overflow: hidden;
transition: border-color .3s;
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.05);
}
.kai-section:hover {
border-color: rgba(45,123,255,0.25);
@@ -160,7 +161,7 @@
.kai-section-header {
padding: 16px 24px 14px;
border-bottom: 1px solid var(--kai-border);
background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.05), transparent);
background: linear-gradient(to right, rgba(var(--kai-blue-rgb), 0.08), transparent);
}
.kai-section-title {
font-family: 'Unbounded', sans-serif;
@@ -200,7 +201,7 @@
@media (max-width: 640px) { .kai-kpi-grid { grid-template-columns: 1fr; } }
.kai-kpi {
background: rgba(255,255,255,0.03);
background: #FFFFFF;
border: 1px solid var(--kai-border);
border-radius: 14px;
padding: 20px;
@@ -212,13 +213,14 @@
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, rgba(45,123,255,0.07), transparent 70%);
background: radial-gradient(circle at top right, rgba(45,123,255,0.09), transparent 70%);
pointer-events: none;
}
.kai-kpi:hover {
border-color: var(--kai-border-hover);
background: var(--kai-card-hover);
transform: translateY(-2px);
box-shadow: 0 12px 26px rgba(45,123,255,0.12);
}
.kai-kpi-icon {
width: 36px;
@@ -276,7 +278,7 @@
.kai-stars { color: var(--kai-yellow); font-size: 16px; letter-spacing: 2px; }
/* Heatmap bars */
.kai-progress-track { background: rgba(255,255,255,0.06); border-radius: 999px; height: 6px; overflow: hidden; }
.kai-progress-track { background: rgba(45,123,255,0.10); border-radius: 999px; height: 6px; overflow: hidden; }
.kai-progress-fill { height: 100%; border-radius: 999px; transition: width 1s ease-out; background: var(--kai-blue); }
/* Info badge strip */
@@ -317,7 +319,7 @@
.kai-table th:hover { color: var(--kai-txt2); }
.kai-table td {
padding: 12px 14px;
border-bottom: 1px solid rgba(255,255,255,0.04);
border-bottom: 1px solid var(--kai-border);
color: var(--kai-txt);
vertical-align: middle;
}
@@ -338,7 +340,7 @@
/* Competitor expander */
.kai-expand-btn { background: none; border: none; cursor: pointer; color: var(--kai-txt2); padding: 4px; border-radius: 6px; transition: all .2s; display: flex; align-items: center; }
.kai-expand-btn:hover { background: rgba(45,123,255,0.1); color: var(--kai-blue); }
.kai-expand-row { background: rgba(10,14,26,0.5); border-top: 1px solid var(--kai-border); }
.kai-expand-row { background: rgba(45,123,255,0.04); border-top: 1px solid var(--kai-border); }
.kai-expand-inner { padding: 16px 20px; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
@media (max-width: 640px) { .kai-expand-inner { grid-template-columns: 1fr; } }
@@ -379,25 +381,26 @@
.kai-conc-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 14px; }
@media (max-width: 1024px) { .kai-conc-grid { grid-template-columns: 1fr; } }
.kai-conc-card {
background: rgba(255,255,255,0.03);
border: 1px solid var(--kai-border);
background: rgba(45,123,255,0.03);
border: 1px solid rgba(45,123,255,0.14);
border-radius: 14px;
padding: 18px 20px;
cursor: default;
transition: all .3s;
}
.kai-conc-card:hover { border-color: var(--kai-border-hover); transform: translateY(-4px); box-shadow: 0 0 24px rgba(45,123,255,0.12); }
.kai-conc-card:hover { border-color: var(--kai-border-hover); transform: translateY(-4px); box-shadow: 0 8px 24px rgba(45,123,255,0.1); }
.kai-conc-icon { font-size: 20px; margin-bottom: 10px; }
.kai-conc-text { font-size: 13px; color: var(--kai-txt); line-height: 1.6; }
/* SMM Rationale */
.kai-rationale {
background: linear-gradient(135deg, #0D1F4A 0%, var(--kai-bg) 100%);
background: linear-gradient(135deg, rgba(45,123,255,0.06) 0%, rgba(16,185,129,0.03) 100%);
border-left: 4px solid var(--kai-blue);
border-radius: 14px;
border: 1px solid rgba(45,123,255,0.18);
border-radius: 16px;
padding: 32px;
}
.kai-rationale-text { font-size: 15px; color: rgba(255,255,255,0.87); line-height: 1.8; white-space: pre-line; }
.kai-rationale-text { font-size: 14px; color: var(--kai-txt); line-height: 1.9; white-space: pre-line; font-weight: 500; }
.kai-cta-box {
margin-top: 28px;
border: 1px solid rgba(45,123,255,0.3);
@@ -435,11 +438,12 @@
z-index: 90;
}
@media (max-width: 1280px) { .kai-sidenav { display: none; } }
/* FIX: was rgba(255,255,255,0.15) — invisible on light background */
.kai-sidenav-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: rgba(255,255,255,0.15);
background: rgba(45,123,255,0.20);
cursor: pointer;
transition: all .2s;
border: none;
@@ -449,11 +453,11 @@
.kai-sidenav-dot.active { background: var(--kai-blue); box-shadow: 0 0 8px rgba(45,123,255,0.6); width: 10px; height: 10px; }
/* Area chart gradient */
.kai-area-chart-wrap { background: rgba(255,255,255,0.02); border-radius: 12px; padding: 16px; }
.kai-area-chart-wrap { background: rgba(45,123,255,0.03); border-radius: 12px; padding: 16px; border: 1px solid rgba(45,123,255,0.07); }
/* Loading skeleton */
.kai-skeleton {
background: rgba(255,255,255,0.05);
background: rgba(45,123,255,0.05);
border-radius: 12px;
position: relative;
overflow: hidden;
@@ -462,15 +466,15 @@
content: '';
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255,255,255,0.04), transparent);
background: linear-gradient(90deg, transparent, rgba(45,123,255,0.06), transparent);
animation: kai-shimmer 1.5s infinite;
}
@keyframes kai-shimmer { 0% { transform: translateX(-100%) } 100% { transform: translateX(100%) } }
/* Factor card */
.kai-factor {
background: rgba(255,255,255,0.03);
border: 1px solid var(--kai-border);
background: rgba(45,123,255,0.04);
border: 1px solid rgba(45,123,255,0.14);
border-radius: 12px;
padding: 16px;
}
@@ -504,7 +508,7 @@
.kai-focus-text { font-size: 13px; font-weight: 600; color: var(--kai-txt); line-height: 1.5; padding-top: 6px; }
/* Search demand badges */
.kai-query-card { padding: 12px 16px; background: rgba(255,255,255,0.03); border: 1px solid var(--kai-border); border-radius: 10px; }
.kai-query-card { padding: 12px 16px; background: rgba(45,123,255,0.04); border: 1px solid rgba(45,123,255,0.14); border-radius: 10px; }
.kai-query-label { font-size: 13px; color: var(--kai-txt); font-weight: 500; margin-bottom: 8px; }
/* CTA tags (behavioral) */
@@ -530,5 +534,5 @@
}
.kai-peak-title { font-weight: 700; color: var(--kai-blue); font-size: 13px; margin-bottom: 4px; }
.kai-peak-sub { font-size: 11px; color: var(--kai-txt2); }
.kai-term-icon { color: rgba(255,255,255,0.4); font-size: 13px; margin-left: 4px; cursor: help; transition: color 0.2s; }
.kai-term-icon { color: rgba(45,123,255,0.4); font-size: 13px; margin-left: 4px; cursor: help; transition: color 0.2s; }
.kai-term-icon:hover { color: #2D7BFF; }
-4
View File
@@ -1,13 +1,9 @@
<script setup>
import AppConfigurator from '@/layout/AppConfigurator.vue';
import { useLayout } from '@/layout/composables/layout';
const { toggleDarkMode, isDarkTheme } = useLayout();
</script>
<template>
<div class="fixed flex gap-4 top-8 right-8">
<Button type="button" @click="toggleDarkMode" rounded :icon="isDarkTheme ? 'pi pi-moon' : 'pi pi-sun'" severity="secondary" />
<div class="relative">
<Button
icon="pi pi-palette"
@@ -299,6 +299,19 @@ const goBack = () => {
}
};
const extractAnalysisId = (result) => {
if (!result) return null;
return (
result.analysisId ||
result.id ||
result.analysis?.id ||
result.analysis?.analysisId ||
result.data?.analysisId ||
result.data?.id ||
null
);
};
// Submit
const handleSubmit = async () => {
// Финальная проверка всех шагов перед отправкой на всякий случай
@@ -327,7 +340,7 @@ const handleSubmit = async () => {
};
const result = await MarketingV3Service.startAnalysisV3(payload);
const analysisId = result.analysisId || result.id;
const analysisId = extractAnalysisId(result);
if (analysisId) {
toast.add({
@@ -337,7 +350,14 @@ const handleSubmit = async () => {
life: 3000
});
router.push(`/marketing-analysis/v3/${analysisId}`);
await router.push({
name: 'marketing-v3-analysis',
params: { id: String(analysisId) }
});
if (router.currentRoute.value.params.id !== String(analysisId)) {
window.location.assign(`/marketing-analysis/v3/${analysisId}`);
}
} else {
throw new Error('Не получен ID анализа');
}
@@ -422,7 +442,7 @@ export default {
<div v-for="(label, index) in STEPS" :key="index"
class="h-1 rounded-full flex-1 transition-all duration-500"
:class="[
index <= currentStep ? 'bg-blue-500 shadow-[0_0_8px_rgba(45,123,255,0.4)]' : 'bg-surface-800'
index <= currentStep ? 'bg-blue-500 shadow-[0_0_8px_rgba(45,123,255,0.4)]' : 'bg-slate-200'
]">
</div>
</div>
@@ -589,7 +609,7 @@ export default {
</div>
<!-- 3. FOOTER SECTION -->
<div class="p-6 border-t border-surface-800 bg-black/20 flex flex-col sm:flex-row items-center justify-between gap-4">
<div class="p-6 border-t border-surface-100 bg-white/80 backdrop-blur-sm flex flex-col sm:flex-row items-center justify-between gap-4">
<Button label="Назад" icon="pi pi-arrow-left" severity="secondary" text class="font-bold text-[11px] uppercase tracking-widest" :disabled="currentStep === 0 || submitting" @click="goBack" />
<div class="flex items-center gap-3 w-full sm:w-auto">
@@ -602,13 +622,6 @@ export default {
</div>
</div>
<!-- AI Status Bar -->
<div class="mt-4 flex items-center justify-center gap-4 text-[9px] font-black uppercase tracking-[0.2em] text-surface-500">
<span class="w-1.5 h-1.5 rounded-full bg-emerald-500 shadow-[0_0_8px_var(--kai-green)]"></span>
KAI Engine: Ready
<span class="w-px h-3 bg-surface-800"></span>
Secure Input Enabled
</div>
</div>
</div>
</template>
@@ -283,7 +283,7 @@ const maxY = computed(() => Math.max(Math.max(...scatter.value.map(d => d.y)) *
.expanded-row td {
padding: 0 !important;
background: rgba(0,0,0,0.2);
background: rgba(45,123,255,0.04);
border-bottom: 1px solid var(--kai-border);
}
@@ -2,80 +2,212 @@
import { computed } from 'vue';
const props = defineProps({ status: String, exec: Object });
const emit = defineEmits(['strategy', 'back']);
const niche = computed(() => props.exec?.geography || '');
</script>
<template>
<header class="kai-header">
<div class="flex items-center gap-3">
<div class="kai-logo">KAI</div>
<div class="kai-title-area">
<div class="kai-title-niche">Market Intelligence · {{ exec?.geography || '' }}</div>
<div class="kai-title-sub">Professional Ed. · KAI Prime (2026)</div>
<header class="v4-header">
<!-- Left: brand + niche info -->
<div class="v4-header__left">
<div class="v4-header__logo">KAI</div>
<div class="v4-header__info">
<div class="v4-header__niche">Маркет Интеллект · {{ niche }}</div>
<div class="v4-header__edition">KAI Prime 2026 · Professional Ed.</div>
</div>
</div>
<div class="flex items-center gap-3 ml-auto">
<div v-if="status === 'COMPLETED'" class="kai-badge-status green">
<span class="w-1 h-1 rounded-full bg-emerald-500 animate-pulse"></span>
Ready
<!-- Right: status + actions -->
<div class="v4-header__right">
<!-- Status badge -->
<div v-if="status === 'COMPLETED'" class="v4-status v4-status--ready">
<span class="v4-status__dot v4-status__dot--green"></span>
Готово
</div>
<div v-else-if="status === 'PROCESSING' || status === 'QUEUED'" class="kai-badge-status yellow">
<span class="w-1 h-1 rounded-full bg-yellow-500 animate-pulse"></span>
Ingesting...
<div v-else-if="status === 'PROCESSING' || status === 'QUEUED'" class="v4-status v4-status--processing">
<span class="v4-status__dot v4-status__dot--yellow"></span>
Обработка...
</div>
<button class="kai-btn-minor" @click="$emit('back')">
<i class="pi pi-list text-[10px]" />
<!-- Back to list -->
<button class="v4-btn-icon" @click="$emit('back')" title="Вернуться к списку анализов">
<i class="pi pi-list"></i>
</button>
<button v-if="status === 'COMPLETED'" class="kai-cta-btn-header" @click="$emit('strategy')">
Generate Strategy
<i class="pi pi-arrow-right ml-1 text-[9px] opacity-60" />
<!-- Generate Strategy -->
<button v-if="status === 'COMPLETED'" class="v4-btn-strategy" @click="$emit('strategy')">
<i class="pi pi-sparkles"></i>
Создать стратегию
</button>
</div>
</header>
</template>
<style scoped>
.kai-btn-minor {
background: var(--kai-card);
border: 1px solid var(--kai-border);
color: var(--kai-txt3);
width: 28px;
height: 28px;
border-radius: 8px;
.v4-header {
position: sticky;
top: 0;
z-index: 100;
height: 60px;
background: rgba(255, 255, 255, 0.94);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
border-bottom: 1px solid rgba(45, 123, 255, 0.14);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 28px;
gap: 16px;
box-shadow: 0 2px 16px rgba(45,123,255,0.06);
}
.v4-header__left {
display: flex;
align-items: center;
gap: 12px;
}
.v4-header__logo {
width: 32px;
height: 32px;
border-radius: 9px;
background: linear-gradient(135deg, #2D7BFF, #1a55cc);
display: flex;
align-items: center;
justify-content: center;
font-family: 'Unbounded', sans-serif;
font-weight: 900;
font-size: 10px;
color: #fff;
flex-shrink: 0;
box-shadow: 0 4px 12px rgba(45,123,255,0.35);
}
.v4-header__info {
display: flex;
flex-direction: column;
gap: 2px;
}
.v4-header__niche {
font-family: 'Unbounded', sans-serif;
font-weight: 700;
font-size: 11px;
color: #0F172A;
line-height: 1;
letter-spacing: -0.01em;
}
.v4-header__edition {
font-size: 9px;
font-weight: 600;
color: #94A3B8;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.v4-header__right {
display: flex;
align-items: center;
gap: 8px;
}
/* Status badge */
.v4-status {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
border-radius: 999px;
font-family: 'Unbounded', sans-serif;
font-size: 9px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
}
.v4-status--ready {
background: rgba(16, 185, 129, 0.1);
border: 1px solid rgba(16, 185, 129, 0.25);
color: #10B981;
}
.v4-status--processing {
background: rgba(245, 158, 11, 0.1);
border: 1px solid rgba(245, 158, 11, 0.25);
color: #F59E0B;
}
.v4-status__dot {
width: 6px;
height: 6px;
border-radius: 50%;
flex-shrink: 0;
}
.v4-status__dot--green {
background: #10B981;
box-shadow: 0 0 6px rgba(16, 185, 129, 0.5);
}
.v4-status__dot--yellow {
background: #F59E0B;
box-shadow: 0 0 6px rgba(245, 158, 11, 0.5);
animation: pulse-yellow 1.2s infinite;
}
@keyframes pulse-yellow {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* Icon button */
.v4-btn-icon {
width: 34px;
height: 34px;
border-radius: 10px;
border: 1px solid rgba(45, 123, 255, 0.18);
background: rgba(45, 123, 255, 0.04);
color: #64748B;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.2s;
cursor: pointer;
transition: all 0.2s;
font-size: 13px;
}
.v4-btn-icon:hover {
background: rgba(45, 123, 255, 0.09);
color: #2D7BFF;
border-color: rgba(45, 123, 255, 0.32);
transform: scale(1.05);
}
.kai-btn-minor:hover {
background: var(--kai-card-hover);
border-color: var(--kai-border-hover);
color: var(--kai-txt);
}
.kai-cta-btn-header {
background: var(--kai-blue);
/* Strategy button */
.v4-btn-strategy {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 8px 18px;
border-radius: 10px;
background: #2D7BFF;
color: #fff;
border: none;
padding: 6px 14px;
border-radius: 8px;
font-family: 'Unbounded', sans-serif;
font-size: 10px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.05em;
transition: all 0.3s;
cursor: pointer;
box-shadow: 0 4px 12px rgba(var(--kai-blue-rgb), 0.2);
transition: all 0.25s;
box-shadow: 0 4px 16px rgba(45, 123, 255, 0.3);
white-space: nowrap;
}
.kai-cta-btn-header:hover {
transform: translateY(-1px);
filter: brightness(1.1);
box-shadow: 0 6px 16px rgba(var(--kai-blue-rgb), 0.3);
.v4-btn-strategy:hover {
background: #1a6bff;
transform: translateY(-2px);
box-shadow: 0 8px 24px rgba(45, 123, 255, 0.45);
}
.v4-btn-strategy i {
font-size: 11px;
}
</style>
@@ -2,58 +2,64 @@
import { ref, onMounted, onUnmounted } from 'vue';
const sections = [
{ id: 's0', label: 'Overview' },
{ id: 's1', label: 'Landscape' },
{ id: 's2', label: 'Geometry' },
{ id: 's3', label: 'Competitors' },
{ id: 's4', label: 'Content' },
{ id: 's5', label: 'Intensity' },
{ id: 's6', label: 'Reputation' },
{ id: 's7', label: 'Behavior' },
{ id: 's8', label: 'Demand' },
{ id: 's9', label: 'Positioning' },
{ id: 's10', label: 'Summary' },
{ id: 's11', label: 'Rationale' },
{ id: 's0', label: 'Обзор', icon: 'pi pi-home' },
{ id: 's1', label: 'Ландшафт', icon: 'pi pi-map' },
{ id: 's2', label: 'География', icon: 'pi pi-map-marker' },
{ id: 's3', label: 'Конкуренты', icon: 'pi pi-users' },
{ id: 's4', label: 'Контент', icon: 'pi pi-file' },
{ id: 's5', label: 'Интенсивность', icon: 'pi pi-bolt' },
{ id: 's6', label: 'Репутация', icon: 'pi pi-star' },
{ id: 's7', label: 'Поведение', icon: 'pi pi-eye' },
{ id: 's8', label: 'Спрос', icon: 'pi pi-chart-line' },
{ id: 's9', label: 'Позиционирование',icon: 'pi pi-crosshairs' },
{ id: 's10', label: 'Выводы', icon: 'pi pi-check-circle' },
{ id: 's11', label: 'Стратегия', icon: 'pi pi-sparkles' },
];
const active = ref('s0');
const go = (id) => {
const el = document.getElementById(id);
if (el) {
window.scrollTo({
top: el.offsetTop - 100,
behavior: 'smooth'
});
window.scrollTo({ top: el.offsetTop - 80, behavior: 'smooth' });
}
};
let obs;
onMounted(() => {
obs = new IntersectionObserver(entries => {
entries.forEach(e => {
if (e.isIntersecting) active.value = e.target.id;
});
}, { threshold: 0.3, rootMargin: '-10% 0px -70% 0px' });
sections.forEach(s => {
const el = document.getElementById(s.id);
if (el) obs.observe(el);
});
entries.forEach(e => { if (e.isIntersecting) active.value = e.target.id; });
}, { threshold: 0.25, rootMargin: '-5% 0px -65% 0px' });
sections.forEach(s => { const el = document.getElementById(s.id); if (el) obs.observe(el); });
});
onUnmounted(() => obs?.disconnect());
</script>
<template>
<nav class="kai-sidenav">
<div class="sidenav-inner">
<button v-for="(s, i) in sections" :key="s.id"
class="sidenav-item"
:class="{ active: active === s.id }"
@click="go(s.id)">
<span class="sidenav-num">{{ i }}</span>
<span class="sidenav-label">{{ s.label }}</span>
</button>
<div class="sidenav-track">
<button
v-for="(s, i) in sections"
:key="s.id"
class="sidenav-item"
:class="{ active: active === s.id }"
@click="go(s.id)"
:title="s.label"
>
<!-- Active line indicator -->
<span class="sidenav-line" :class="{ 'sidenav-line--active': active === s.id }"></span>
<!-- Number dot -->
<span class="sidenav-dot">
<i v-if="active === s.id" :class="s.icon" class="sidenav-icon-active"></i>
<span v-else class="sidenav-num">{{ i + 1 }}</span>
</span>
<!-- Tooltip label -->
<span class="sidenav-label">{{ s.label }}</span>
</button>
</div>
</div>
</nav>
</template>
@@ -61,91 +67,144 @@ onUnmounted(() => obs?.disconnect());
<style scoped>
.kai-sidenav {
position: fixed;
left: 24px;
left: 16px;
top: 50%;
transform: translateY(-50%);
z-index: 100;
display: flex;
flex-direction: column;
z-index: 200;
pointer-events: none;
}
.sidenav-inner {
pointer-events: auto;
background: rgba(255, 255, 255, 0.92);
backdrop-filter: blur(16px);
-webkit-backdrop-filter: blur(16px);
border: 1px solid rgba(45, 123, 255, 0.18);
border-radius: 20px;
padding: 10px 6px;
box-shadow: 0 8px 32px rgba(45, 123, 255, 0.1), 0 2px 8px rgba(0,0,0,0.06);
}
.sidenav-track {
display: flex;
flex-direction: column;
gap: 8px;
pointer-events: auto;
padding: 12px 8px;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(10px);
border: 1px solid var(--kai-border);
border-radius: 16px;
gap: 2px;
}
.sidenav-item {
position: relative;
display: flex;
align-items: center;
gap: 0;
padding: 5px 4px;
border-radius: 10px;
background: transparent;
border: none;
cursor: pointer;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
overflow: visible;
}
.sidenav-item:hover {
background: rgba(45, 123, 255, 0.06);
}
.sidenav-item.active {
background: rgba(45, 123, 255, 0.1);
}
/* Left accent line */
.sidenav-line {
position: absolute;
left: -6px;
top: 50%;
transform: translateY(-50%);
width: 3px;
height: 0;
background: var(--kai-blue, #2D7BFF);
border-radius: 4px;
transition: height 0.25s ease;
}
.sidenav-line--active {
height: 60%;
}
/* Dot */
.sidenav-dot {
width: 28px;
height: 28px;
border-radius: 8px;
background: transparent;
border: 1px solid transparent;
display: flex;
align-items: center;
justify-content: center;
position: relative;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: visible;
flex-shrink: 0;
transition: all 0.25s;
}
.sidenav-num {
font-family: 'Unbounded', sans-serif;
font-size: 8px;
font-size: 8.5px;
font-weight: 800;
color: var(--kai-txt3);
transition: all 0.3s;
color: #94A3B8;
transition: color 0.25s;
line-height: 1;
}
.sidenav-item:hover .sidenav-num {
color: #2D7BFF;
}
.sidenav-item.active .sidenav-num {
color: #2D7BFF;
}
.sidenav-icon-active {
font-size: 11px;
color: #2D7BFF;
}
/* Tooltip */
.sidenav-label {
position: absolute;
left: 40px;
background: var(--kai-bg);
border: 1px solid var(--kai-border);
color: var(--kai-txt);
padding: 4px 10px;
border-radius: 6px;
left: calc(100% + 12px);
top: 50%;
transform: translateY(-50%);
background: #0F172A;
color: #fff;
padding: 5px 10px;
border-radius: 8px;
font-size: 10px;
font-weight: 700;
font-family: 'Onest', sans-serif;
white-space: nowrap;
opacity: 0;
transform: translateX(-10px);
pointer-events: none;
transition: all 0.2s;
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
transform: translateY(-50%) translateX(-6px);
box-shadow: 0 4px 16px rgba(0,0,0,0.2);
letter-spacing: 0.01em;
}
.sidenav-label::before {
content: '';
position: absolute;
right: 100%;
top: 50%;
transform: translateY(-50%);
border: 5px solid transparent;
border-right-color: #0F172A;
}
.sidenav-item:hover .sidenav-label {
opacity: 1;
transform: translateX(0);
transform: translateY(-50%) translateX(0);
}
.sidenav-item:hover {
background: var(--kai-card-hover);
border-color: var(--kai-border);
}
.sidenav-item.active {
background: var(--kai-blue);
border-color: var(--kai-blue);
box-shadow: 0 0 15px rgba(var(--kai-blue-rgb), 0.4);
}
.sidenav-item.active .sidenav-num {
color: #fff;
.sidenav-item.active .sidenav-label {
opacity: 1;
transform: translateY(-50%) translateX(0);
}
@media (max-width: 1400px) {
.kai-sidenav {
display: none;
}
.kai-sidenav { display: none; }
}
</style>
+13 -2
View File
@@ -37,9 +37,20 @@ const model = ref([
]
},
{
label: 'Admin',
label: 'Сервисы',
items: [
{ label: 'Users', icon: 'pi pi-fw pi-users', to: '/admin/users' },
{
label: 'CRM колл-центр',
icon: 'pi pi-fw pi-external-link',
url: 'https://call-center.konturai.kz/operator#workspace',
target: '_blank'
}
]
},
{
label: 'Администрирование',
items: [
{ label: 'Пользователи', icon: 'pi pi-fw pi-users', to: '/admin/users' },
{ label: 'Управление парсером', icon: 'pi pi-fw pi-cog', to: '/admin/news' }
]
}
+9 -1
View File
@@ -71,7 +71,15 @@ function checkActiveRoute(item) {
<template>
<li :class="{ 'layout-root-menuitem': root, 'active-menuitem': isActiveMenu }">
<div v-if="root && item.visible !== false" class="layout-menuitem-root-text">{{ item.label }}</div>
<a v-if="(!item.to || item.items) && item.visible !== false" :href="item.url" @click="itemClick($event, item, index)" :class="item.class" :target="item.target" tabindex="0">
<a
v-if="(!item.to || item.items) && item.visible !== false"
:href="item.url"
@click="itemClick($event, item, index)"
:class="item.class"
:target="item.target"
:rel="item.target === '_blank' ? 'noopener noreferrer' : null"
tabindex="0"
>
<i :class="item.icon" class="layout-menuitem-icon"></i>
<span class="layout-menuitem-text">{{ item.label }}</span>
<i class="pi pi-fw pi-angle-down layout-submenu-toggler" v-if="item.items"></i>
+2 -5
View File
@@ -4,7 +4,7 @@ import AuthService from '@/service/AuthService';
import { useRouter } from 'vue-router';
import AppConfigurator from './AppConfigurator.vue';
const { toggleMenu, toggleDarkMode, isDarkTheme } = useLayout();
const { toggleMenu } = useLayout();
const router = useRouter();
async function onLogout() {
@@ -44,9 +44,6 @@ async function onLogout() {
<div class="layout-topbar-actions">
<div class="layout-config-menu">
<button type="button" class="layout-topbar-action" @click="toggleDarkMode">
<i :class="['pi', { 'pi-moon': isDarkTheme, 'pi-sun': !isDarkTheme }]"></i>
</button>
<div class="relative">
<button
v-styleclass="{ selector: '@next', enterFromClass: 'hidden', enterActiveClass: 'animate-scalein', leaveToClass: 'hidden', leaveActiveClass: 'animate-fadeout', hideOnOutsideClick: true }"
@@ -78,7 +75,7 @@ async function onLogout() {
</button> -->
<button type="button" class="layout-topbar-action" @click="onLogout">
<i class="pi pi-sign-out"></i>
<span>Logout</span>
<span>Выйти</span>
</button>
</div>
</div>
+6 -9
View File
@@ -24,18 +24,13 @@ export function useLayout() {
};
const toggleDarkMode = () => {
if (!document.startViewTransition) {
executeDarkModeToggle();
return;
}
document.startViewTransition(() => executeDarkModeToggle(event));
layoutConfig.darkTheme = false;
document.documentElement.classList.remove('app-dark');
};
const executeDarkModeToggle = () => {
layoutConfig.darkTheme = !layoutConfig.darkTheme;
document.documentElement.classList.toggle('app-dark');
layoutConfig.darkTheme = false;
document.documentElement.classList.remove('app-dark');
};
const toggleMenu = () => {
@@ -58,6 +53,8 @@ export function useLayout() {
const getSurface = computed(() => layoutConfig.surface);
executeDarkModeToggle();
return {
layoutConfig,
layoutState,
+2
View File
@@ -12,6 +12,8 @@ import '@/assets/styles.scss';
import '@/assets/kai-v4.css';
import 'katex/dist/katex.min.css';
document.documentElement.classList.remove('app-dark');
const app = createApp(App);
const pinia = createPinia();
+16 -3
View File
@@ -1,13 +1,26 @@
<script>
export default {
name: 'DashboardNew'
name: 'DashboardNew',
methods: {
openCrm() {
const url = 'https://call-center.konturai.kz/operator#workspace';
const newWindow = window.open(url, '_blank', 'noopener,noreferrer');
if (newWindow) newWindow.opener = null;
}
}
};
</script>
<template>
<div class="card">
<h1 class="text-2xl font-semibold">New Dashboard</h1>
<p class="text-color-secondary">Страница пока пустая.</p>
<div class="flex items-center justify-between gap-4 flex-wrap">
<div>
<h1 class="text-2xl font-semibold">Главный экран</h1>
<p class="text-color-secondary">Быстрые ссылки и основные разделы.</p>
</div>
<Button label="CRM Call-Center" icon="pi pi-external-link" severity="secondary" @click="openCrm" />
</div>
</div>
</template>
+2 -2
View File
@@ -67,9 +67,9 @@ async function onSubmit() {
<div class="flex items-center justify-between">
<div class="flex items-center">
<Checkbox v-model="checked" id="rememberme1" binary inputId="rememberme1" />
<label for="rememberme1" class="ml-2 text-sm text-surface-600 dark:text-surface-400">Remember me</label>
<label for="rememberme1" class="ml-2 text-sm text-surface-600 dark:text-surface-400">Запомнить меня</label>
</div>
<span class="cursor-pointer text-sm font-medium text-primary no-underline hover:text-primary-600 dark:text-primary-400"> Forgot password? </span>
<span class="cursor-pointer text-sm font-medium text-primary no-underline hover:text-primary-600 dark:text-primary-400"> Забыли пароль? </span>
</div>
<Button :loading="loading" label="Войти" icon="pi pi-sign-in" icon-pos="right" class="w-full" @click="onSubmit" />
@@ -1,24 +1,23 @@
<script setup>
import '@/assets/kai-v4.css';
import MarketingV3Service from '@/service/MarketingV3Service';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import '@/assets/kai-v4.css';
// ─── Sections ────────────────────────────────────────────────────────────────
import V4Header from '@/components/marketing/dashboard-v3/v4/V4Header.vue';
import V4Executive from '@/components/marketing/dashboard-v3/v4/V4Executive.vue';
import V4Landscape from '@/components/marketing/dashboard-v3/v4/V4Landscape.vue';
import V4Geo from '@/components/marketing/dashboard-v3/v4/V4Geo.vue';
import V4Competitors from '@/components/marketing/dashboard-v3/v4/V4Competitors.vue';
import V4Content from '@/components/marketing/dashboard-v3/v4/V4Content.vue';
import V4Cii from '@/components/marketing/dashboard-v3/v4/V4Cii.vue';
import V4Reputation from '@/components/marketing/dashboard-v3/v4/V4Reputation.vue';
import V4Behavioral from '@/components/marketing/dashboard-v3/v4/V4Behavioral.vue';
import V4Demand from '@/components/marketing/dashboard-v3/v4/V4Demand.vue';
import V4Positioning from '@/components/marketing/dashboard-v3/v4/V4Positioning.vue';
import V4Conclusions from '@/components/marketing/dashboard-v3/v4/V4Conclusions.vue';
import V4Rationale from '@/components/marketing/dashboard-v3/v4/V4Rationale.vue';
import V4SideNav from '@/components/marketing/dashboard-v3/v4/V4SideNav.vue';
import V4Behavioral from '@/components/marketing/dashboard-v3/v4/V4Behavioral.vue';
import V4Cii from '@/components/marketing/dashboard-v3/v4/V4Cii.vue';
import V4Competitors from '@/components/marketing/dashboard-v3/v4/V4Competitors.vue';
import V4Conclusions from '@/components/marketing/dashboard-v3/v4/V4Conclusions.vue';
import V4Content from '@/components/marketing/dashboard-v3/v4/V4Content.vue';
import V4Demand from '@/components/marketing/dashboard-v3/v4/V4Demand.vue';
import V4Executive from '@/components/marketing/dashboard-v3/v4/V4Executive.vue';
import V4Geo from '@/components/marketing/dashboard-v3/v4/V4Geo.vue';
import V4Header from '@/components/marketing/dashboard-v3/v4/V4Header.vue';
import V4Landscape from '@/components/marketing/dashboard-v3/v4/V4Landscape.vue';
import V4Positioning from '@/components/marketing/dashboard-v3/v4/V4Positioning.vue';
import V4Rationale from '@/components/marketing/dashboard-v3/v4/V4Rationale.vue';
import V4Reputation from '@/components/marketing/dashboard-v3/v4/V4Reputation.vue';
const route = useRoute();
const router = useRouter();
@@ -29,7 +28,7 @@ const status = ref('IDLE');
const result = ref(null);
const errorMsg = ref('');
const MSGS = ['Data Ingestion: Active','Contextual Mapping...','Competitor Core Analysis...','Sentiment Extraction...','Synthesis & Prioritization...'];
const MSGS = ['Сбор данных активен', 'Контекстная маршрутизация...', 'Анализ ядра конкурентов...', 'Извлечение тональности...', 'Синтез и приоритизация...'];
const loaderMsg = ref(MSGS[0]);
const loaderDot = ref(1);
let pollInterval = null;
@@ -76,7 +75,8 @@ const fetchAnalysis = async () => {
loaderMsg.value = MSGS[++tick % MSGS.length];
loaderDot.value = (loaderDot.value % 5) + 1;
} catch (e) {
status.value = 'FAILED'; errorMsg.value = e.message || 'Ошибка загрузки.';
status.value = 'FAILED';
errorMsg.value = e.message || 'Ошибка загрузки анализа.';
} finally { loading.value = false; }
};
@@ -97,8 +97,8 @@ onUnmounted(() => stopPolling());
<!-- Sticky Header -->
<V4Header :status="status" :exec="exec" @strategy="goStrategy" @back="$router.push('/marketing-analysis/v3')" />
<!-- Side Nav -->
<V4SideNav v-if="status === 'COMPLETED'" />
<!-- Side Nav - DISABLED -->
<!-- <V4SideNav v-if="status === 'COMPLETED'" /> -->
<div class="analysis-container">
@@ -111,7 +111,7 @@ onUnmounted(() => stopPolling());
</div>
<div class="loading-info">
<div class="loading-title">{{ loaderMsg }}</div>
<p class="loading-sub">KAI Prime Engine is synthesising market intelligence...</p>
<p class="loading-sub">KAI Prime обрабатывает данные и формирует маркетинговую аналитику...</p>
<div class="loading-dots">
<span v-for="i in 5" :key="i" class="dot" :class="{ active: loaderDot === i }"></span>
</div>
@@ -123,10 +123,10 @@ onUnmounted(() => stopPolling());
<div class="error-icon">
<i class="pi pi-exclamation-triangle" />
</div>
<h2 class="error-title">Analysis Interrupted</h2>
<p class="error-desc">{{ errorMsg || 'An unexpected error occurred during intelligence synthesis.' }}</p>
<h2 class="error-title">Анализ прерван</h2>
<p class="error-desc">{{ errorMsg || 'Во время анализа произошла непредвиденная ошибка.' }}</p>
<button class="kai-btn-secondary" @click="$router.push('/marketing-analysis/v3')">
<i class="pi pi-arrow-left mr-2" /> Return to Command Center
<i class="pi pi-arrow-left mr-2" /> Вернуться к списку анализов
</button>
</div>
File diff suppressed because it is too large Load Diff
+54 -26
View File
@@ -2,6 +2,7 @@
import { useRouter } from 'vue-router';
const router = useRouter();
const nav = (path, query) => query ? router.push({ path, query }) : router.push(path);
const navExternal = (url) => window.open(url, '_blank');
</script>
<template>
@@ -17,7 +18,7 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
<div class="mkt-hub__header">
<div class="mkt-hub__badge">
<span class="mkt-hub__badge-dot"></span>
KAI Intelligence · Prime 2026
KAI Intelligence · Платформа 2026
</div>
<h1 class="mkt-hub__title">Маркетинг-Хаб</h1>
<p class="mkt-hub__sub">Автономное управление маркетингом: от глубоких исследований рынка до генерации контента и запуска рекламных кампаний.</p>
@@ -67,6 +68,26 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
<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')">
<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>
<!-- CRM -->
<div class="mkt-card mkt-card--teal" @click="nav('/crm')">
<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 -->
@@ -79,7 +100,7 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
<div>
<div class="mkt-banner__label">
<span class="mkt-banner__dot"></span>
Beta API
Бета-режим API
</div>
<h2 class="mkt-banner__title">ИИ-Таргетолог</h2>
<p class="mkt-banner__desc">Развёртывание рекламных кампаний ИИ создаст аудитории, оптимизирует бюджет и запустит рекламу в Facebook, Instagram и TikTok</p>
@@ -98,12 +119,13 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
<style scoped>
/* ───── Page layout ───── */
.mkt-hub {
background: var(--kai-bg);
background: #F5F8FF;
min-height: 100vh;
font-family: 'Onest', sans-serif;
position: relative;
overflow: hidden;
padding: 48px 24px 80px;
color: #0F172A;
}
.mkt-hub__glow1 {
@@ -144,22 +166,22 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
gap: 6px;
padding: 4px 12px;
border-radius: 999px;
border: 1px solid var(--kai-border);
background: rgba(var(--kai-blue-rgb), 0.08);
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;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--kai-blue);
color: #2D7BFF;
margin-bottom: 16px;
}
.mkt-hub__badge-dot {
width: 5px;
height: 5px;
border-radius: 50%;
background: var(--kai-blue);
box-shadow: 0 0 8px var(--kai-blue);
background: #2D7BFF;
box-shadow: 0 0 8px #2D7BFF;
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
@@ -171,7 +193,7 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
font-family: 'Unbounded', sans-serif;
font-size: 32px;
font-weight: 900;
color: var(--kai-txt);
color: #0F172A;
letter-spacing: -0.03em;
line-height: 1.1;
margin-bottom: 12px;
@@ -179,7 +201,7 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
.mkt-hub__sub {
font-size: 13px;
color: var(--kai-txt2);
color: #334155;
max-width: 500px;
margin: 0 auto;
line-height: 1.6;
@@ -189,24 +211,28 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
/* ───── Grid ───── */
.mkt-hub__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
grid-template-columns: repeat(3, 1fr);
gap: 16px;
margin-bottom: 16px;
}
@media (max-width: 900px) {
.mkt-hub__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 640px) {
.mkt-hub__grid { grid-template-columns: 1fr; }
}
/* ───── Cards ───── */
.mkt-card {
background: var(--kai-card);
border: 1px solid var(--kai-border);
background: #FFFFFF;
border: 1px solid rgba(45,123,255,0.18);
border-radius: 20px;
padding: 28px;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
position: relative;
overflow: hidden;
box-shadow: 0 2px 8px rgba(45,123,255,0.05);
}
.mkt-card::before {
content: '';
@@ -219,16 +245,17 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
}
.mkt-card:hover {
transform: translateY(-4px);
border-color: var(--kai-border-hover);
background: var(--kai-card-hover);
box-shadow: 0 20px 60px rgba(0,0,0,0.4), 0 0 40px rgba(45,123,255,0.08);
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);
}
.mkt-card:hover::before { opacity: 1; }
.mkt-card--blue::before { background: radial-gradient(circle at top right, rgba(45,123,255,0.1), transparent 60%); }
.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%); }
.mkt-card__icon {
width: 52px;
@@ -247,14 +274,14 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
font-family: 'Unbounded', sans-serif;
font-size: 14px;
font-weight: 700;
color: var(--kai-txt);
color: #0F172A;
margin-bottom: 8px;
line-height: 1.25;
}
.mkt-card__desc {
font-size: 11.5px;
color: var(--kai-txt2);
color: #334155;
line-height: 1.55;
font-weight: 400;
}
@@ -296,9 +323,9 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
margin-top: 12px;
padding: 2px 8px;
border-radius: 999px;
border: 1px solid rgba(var(--kai-green-rgb), 0.25);
background: rgba(var(--kai-green-rgb), 0.07);
color: var(--kai-green);
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;
@@ -308,8 +335,8 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
/* ───── Targetologist Banner ───── */
.mkt-banner {
background: linear-gradient(135deg, rgba(var(--kai-blue-rgb), 0.08) 0%, rgba(139,92,246,0.05) 100%);
border: 1px solid var(--kai-border);
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;
@@ -320,6 +347,7 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
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);
@@ -385,14 +413,14 @@ const nav = (path, query) => query ? router.push({ path, query }) : router.push(
font-family: 'Unbounded', sans-serif;
font-size: 20px;
font-weight: 900;
color: var(--kai-txt);
color: #0F172A;
margin-bottom: 6px;
line-height: 1.2;
}
.mkt-banner__desc {
font-size: 12px;
color: var(--kai-txt2);
color: #334155;
line-height: 1.5;
max-width: 520px;
}
@@ -1,11 +1,10 @@
<script setup>
import { DEFAULT_STRATEGY_MODEL, STRATEGY_MODELS } from '@/config/marketingModels';
import MarketingV3Service from '@/service/MarketingV3Service';
import { STRATEGY_MODELS, DEFAULT_STRATEGY_MODEL } from '@/config/marketingModels';
import { useTargetingStore } from '@/stores/targeting';
import Button from 'primevue/button';
import Dialog from 'primevue/dialog';
import Tag from 'primevue/tag';
import { useToast } from 'primevue/usetoast';
import { useTargetingStore } from '@/stores/targeting';
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
@@ -2059,7 +2058,21 @@ onUnmounted(() => {
.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; } }
.premium-modal {
:deep(.p-dialog-content) { background: transparent !important; }
:deep(.p-dialog) {
background: rgba(0, 0, 0, 0.7) !important;
backdrop-filter: blur(10px);
border: none !important;
}
:deep(.p-dialog-header) {
background: transparent !important;
border: none !important;
}
:deep(.p-dialog-mask) {
background: transparent !important;
}
}
.modal-content-wrapper { position: relative; max-width: 900px; margin: 0 auto; }
.modal-close-trigger { position: absolute; top: -40px; right: 0; color: #fff !important; background: rgba(255,255,255,0.1) !important; }
.modal-media-container { background: #000; border-radius: 24px; overflow: hidden; border: 1px solid rgba(255,255,255,0.1); }
@@ -58,11 +58,11 @@ const formatDate = dt => dt ? new Date(dt).toLocaleString('ru-RU', { day: '2-dig
const statusLabel = s => ({ COMPLETED: 'Завершена', PROCESSING: 'В работе', QUEUED: 'В очереди', FAILED: 'Ошибка' })[(s||'').toUpperCase()] || s;
const statusStyle = s => ({
COMPLETED: { color: 'var(--kai-green)', bg: 'rgba(0,212,139,0.1)', border: 'rgba(0,212,139,0.25)' },
PROCESSING: { color: 'var(--kai-blue)', bg: 'rgba(45,123,255,0.1)', border: 'rgba(45,123,255,0.25)' },
QUEUED: { color: 'var(--kai-yellow)', bg: 'rgba(245,158,11,0.1)', border: 'rgba(245,158,11,0.25)' },
FAILED: { color: 'var(--kai-red)', bg: 'rgba(239,68,68,0.1)', border: 'rgba(239,68,68,0.25)' }
})[(s||'').toUpperCase()] || { color: 'var(--kai-txt3)', bg: 'rgba(255,255,255,0.05)', border: 'rgba(255,255,255,0.1)' };
COMPLETED: { color: '#10B981', bg: 'rgba(16,185,129,0.09)', border: 'rgba(16,185,129,0.22)', accent: '#10B981' },
PROCESSING: { color: '#2D7BFF', bg: 'rgba(45,123,255,0.09)', border: 'rgba(45,123,255,0.22)', accent: '#2D7BFF' },
QUEUED: { color: '#F59E0B', bg: 'rgba(245,158,11,0.09)', border: 'rgba(245,158,11,0.22)', accent: '#F59E0B' },
FAILED: { color: '#EF4444', bg: 'rgba(239,68,68,0.09)', border: 'rgba(239,68,68,0.22)', accent: '#EF4444' }
})[(s||'').toUpperCase()] || { color: '#64748B', bg: 'rgba(100,116,139,0.08)', border: 'rgba(100,116,139,0.18)', accent: '#64748B' };
const statusIcon = s => ({
COMPLETED: 'pi pi-check-circle',
@@ -71,13 +71,19 @@ const statusIcon = s => ({
FAILED: 'pi pi-times-circle'
})[(s||'').toUpperCase()] || 'pi pi-circle';
const getPlatformIcon = p => {
const pl = (p||'').toLowerCase();
if (pl.includes('insta')) return 'pi pi-instagram';
if (pl.includes('tik')) return 'pi pi-tiktok';
if (pl.includes('you')) return 'pi pi-youtube';
if (pl.includes('tg') || pl.includes('tele')) return 'pi pi-telegram';
return 'pi pi-share-alt';
const PLAT_META = {
instagram: { icon: 'pi pi-instagram', color: '#E1306C', bg: 'rgba(225,48,108,0.1)' },
facebook: { icon: 'pi pi-facebook', color: '#1877F2', bg: 'rgba(24,119,242,0.1)' },
tiktok: { icon: 'pi pi-tiktok', color: '#69C9D0', bg: 'rgba(105,201,208,0.1)' },
youtube: { icon: 'pi pi-youtube', color: '#FF0000', bg: 'rgba(255,0,0,0.1)' },
telegram: { icon: 'pi pi-telegram', color: '#2AABEE', bg: 'rgba(42,171,238,0.1)' },
};
const getPlatMeta = p => {
const key = (p || '').toLowerCase();
for (const [k, v] of Object.entries(PLAT_META)) {
if (key.includes(k)) return v;
}
return { icon: 'pi pi-share-alt', color: '#64748B', bg: 'rgba(100,116,139,0.1)' };
};
const getModelInfo = modelName => {
@@ -93,7 +99,7 @@ const getStrategyName = item =>
const getLatestMsg = item => {
if (item.statusHistory?.length > 0) return item.statusHistory[item.statusHistory.length - 1].message;
return item.status === 'QUEUED' ? 'В очереди...' : 'Фабрика генерирует контент...';
return item.status === 'QUEUED' ? 'Ожидание в очереди...' : 'Контент-фабрика генерирует материалы...';
};
const openStrategy = item => {
@@ -111,74 +117,111 @@ onMounted(load);
<div class="sl-page">
<Toast />
<div class="sl-glow sl-glow--green"></div>
<div class="sl-glow sl-glow--blue"></div>
<div class="sl-inner">
<!-- HEADER -->
<div class="sl-header">
<div class="sl-header__left">
<button class="sl-back-btn" @click="$router.push('/marketing-analysis')">
<button class="sl-back-btn" @click="$router.push('/marketing-analysis')" title="Назад">
<i class="pi pi-arrow-left"></i>
</button>
<div>
<div class="sl-eyebrow">Content Factory · v3</div>
<div class="sl-eyebrow">
<span class="sl-eyebrow__dot"></span>
Контент-Фабрика · v3
</div>
<h1 class="sl-title">Мои стратегии</h1>
</div>
</div>
<button class="sl-btn sl-btn--ghost" @click="$router.push('/marketing-analysis/v3')">
<i class="pi pi-chart-bar"></i> Мои анализы
<i class="pi pi-chart-bar"></i>
<span>Мои анализы</span>
</button>
</div>
<!-- STATS -->
<div class="sl-stats">
<div class="sl-stat" v-for="f in STATUS_FILTERS.filter(f=>f.value)" :key="f.value">
<span class="sl-stat__num" :style="{ color: statusStyle(f.value).color }">{{ countByStatus(f.value) }}</span>
<span class="sl-stat__label">{{ f.label }}</span>
<div class="sl-stats-row">
<div class="sl-stat-card" v-for="f in STATUS_FILTERS.filter(f => f.value)" :key="f.value"
:style="{ borderColor: statusStyle(f.value).border }">
<div class="sl-stat-card__icon" :style="{ background: statusStyle(f.value).bg }">
<i :class="statusIcon(f.value)" :style="{ color: statusStyle(f.value).color }"></i>
</div>
<div class="sl-stat-card__body">
<div class="sl-stat-card__num" :style="{ color: statusStyle(f.value).color }">
{{ countByStatus(f.value) }}
</div>
<div class="sl-stat-card__label">{{ f.label }}</div>
</div>
</div>
<div class="sl-stat">
<span class="sl-stat__num" style="color:var(--kai-txt);">{{ strategies.length }}</span>
<span class="sl-stat__label">Всего</span>
<div class="sl-stat-card sl-stat-card--total">
<div class="sl-stat-card__icon" style="background: rgba(16,185,129,0.09);">
<i class="pi pi-sparkles" style="color: #10B981;"></i>
</div>
<div class="sl-stat-card__body">
<div class="sl-stat-card__num" style="color: #0F172A;">{{ strategies.length }}</div>
<div class="sl-stat-card__label">Всего стратегий</div>
</div>
</div>
</div>
<!-- FILTERS -->
<div class="sl-filters">
<!-- TOOLBAR -->
<div class="sl-toolbar">
<div class="sl-search">
<i class="pi pi-search sl-search__icon"></i>
<input v-model="searchQuery" type="text" placeholder="Поиск (ниша, ID)..." class="sl-search__input" />
<input v-model="searchQuery" type="text" placeholder="Поиск по нише или ID..." class="sl-search__input" />
<button v-if="searchQuery" class="sl-search__clear" @click="searchQuery = ''" title="Очистить">
<i class="pi pi-times"></i>
</button>
</div>
<div class="sl-pills">
<button v-for="f in STATUS_FILTERS" :key="f.value"
@click="filterStatus = f.value"
@click="filterStatus = f.value; currentPage = 1"
class="sl-pill" :class="{ 'sl-pill--active': filterStatus === f.value }">
{{ f.label }}
<span v-if="f.value" class="sl-pill__count">{{ countByStatus(f.value) }}</span>
</button>
<button class="sl-icon-btn" @click="load" title="Обновить">
<i class="pi pi-refresh" :class="{ 'pi-spin': loading }"></i>
<span v-if="f.value && countByStatus(f.value) > 0" class="sl-pill__badge">
{{ countByStatus(f.value) }}
</span>
</button>
</div>
<button class="sl-refresh-btn" @click="load" :title="loading ? 'Загрузка...' : 'Обновить'">
<i class="pi pi-refresh" :class="{ 'pi-spin': loading }"></i>
<span>Обновить</span>
</button>
</div>
<!-- LOADING -->
<div v-if="loading" class="sl-skeleton-list">
<div v-for="i in 4" :key="i" class="sl-skeleton-item">
<div class="sl-skeleton-icon"></div>
<div class="sl-skeleton-icon shimmer"></div>
<div class="sl-skeleton-lines">
<div class="sl-skeleton-line sl-skeleton-line--w40"></div>
<div class="sl-skeleton-line sl-skeleton-line--w60"></div>
<div class="sl-skeleton-line shimmer" style="width: 40%;"></div>
<div class="sl-skeleton-line shimmer" style="width: 65%;"></div>
</div>
<div class="sl-skeleton-badge shimmer"></div>
</div>
</div>
<!-- EMPTY -->
<div v-else-if="!filtered.length" class="sl-empty">
<div class="sl-empty__icon"><i class="pi pi-sparkles"></i></div>
<div class="sl-empty__visual">
<div class="sl-empty__ring"></div>
<div class="sl-empty__icon">
<i class="pi pi-sparkles"></i>
</div>
</div>
<h2 class="sl-empty__title">{{ filterStatus ? 'Нет стратегий с таким статусом' : 'Стратегий пока нет' }}</h2>
<p class="sl-empty__desc">Сгенерируйте первую стратегию ИИ-маркетологом на основе готового анализа.</p>
<button class="sl-btn sl-btn--primary" @click="$router.push('/marketing-analysis/v3')">
<i class="pi pi-arrow-right"></i> К анализам
</button>
<div class="sl-empty__actions">
<button v-if="filterStatus || searchQuery" class="sl-btn sl-btn--ghost" @click="filterStatus = ''; searchQuery = ''">
<i class="pi pi-filter-slash"></i> Сбросить
</button>
<button class="sl-btn sl-btn--primary" @click="$router.push('/marketing-analysis/v3')">
<i class="pi pi-arrow-right"></i> К анализам
</button>
</div>
</div>
<!-- LIST -->
@@ -188,7 +231,10 @@ onMounted(load);
:class="{ 'sl-item--clickable': ['COMPLETED','PROCESSING','QUEUED'].includes((item.status||'').toUpperCase()) }"
@click="openStrategy(item)">
<!-- Status -->
<!-- Accent stripe -->
<div class="sl-item__stripe" :style="{ background: statusStyle(item.status).accent }"></div>
<!-- Status icon -->
<div class="sl-item__status"
:style="{ background: statusStyle(item.status).bg, border: '1px solid ' + statusStyle(item.status).border }">
<i :class="statusIcon(item.status)" :style="{ color: statusStyle(item.status).color }"></i>
@@ -196,11 +242,13 @@ onMounted(load);
<!-- Content -->
<div class="sl-item__content">
<div class="sl-item__row1">
<div class="sl-item__top">
<h3 class="sl-item__name">{{ getStrategyName(item) }}</h3>
<div class="sl-item__badges">
<span class="sl-item__badge"
:style="{ color: statusStyle(item.status).color, background: statusStyle(item.status).bg, borderColor: statusStyle(item.status).border }">
<i :class="statusIcon(item.status)"></i>
{{ statusLabel(item.status) }}
</span>
<span class="sl-item__model"
@@ -210,42 +258,64 @@ onMounted(load);
</div>
</div>
<!-- Meta chips -->
<div class="sl-item__meta">
<span v-if="item.durationWeeks || item.strategyData?.durationWeeks">
<i class="pi pi-calendar"></i> {{ item.durationWeeks || item.strategyData?.durationWeeks }} нед.
<span v-if="item.durationWeeks || item.strategyData?.durationWeeks" class="sl-meta-chip">
<i class="pi pi-calendar"></i>
{{ item.durationWeeks || item.strategyData?.durationWeeks }} нед.
</span>
<span v-if="(item.priorityPlatforms || item.strategyData?.priorityPlatforms)?.length" class="sl-platforms">
<i class="pi pi-sitemap"></i>
<span v-for="p in (item.priorityPlatforms || item.strategyData?.priorityPlatforms)" :key="p" class="sl-plat-chip">
<i :class="getPlatformIcon(p)"></i> {{ p }}
<span class="sl-meta-chip">
<i class="pi pi-clock"></i>
{{ formatDate(item.createdAt) }}
</span>
<!-- Platforms -->
<template v-if="(item.priorityPlatforms || item.strategyData?.priorityPlatforms)?.length">
<span
v-for="p in (item.priorityPlatforms || item.strategyData?.priorityPlatforms)"
:key="p"
class="sl-plat-chip"
:style="{ background: getPlatMeta(p).bg, color: getPlatMeta(p).color }"
>
<i :class="getPlatMeta(p).icon"></i>
{{ p }}
</span>
</span>
<span><i class="pi pi-clock"></i> {{ formatDate(item.createdAt) }}</span>
</template>
</div>
<!-- Progress -->
<div v-if="['PROCESSING','QUEUED'].includes((item.status||'').toUpperCase())" class="sl-item__progress">
<span class="sl-item__progress-msg">{{ getLatestMsg(item) }}</span>
<div class="sl-item__progress-track">
<div class="sl-item__progress-bar" :style="{ width: item.status === 'QUEUED' ? '8%' : '60%' }"></div>
<div class="sl-item__progress-fill"
:style="{ width: item.status === 'QUEUED' ? '8%' : '60%', background: statusStyle(item.status).accent }">
</div>
</div>
<span class="sl-item__progress-msg">{{ getLatestMsg(item) }}</span>
</div>
</div>
<!-- Action -->
<div class="sl-item__action" @click.stop>
<div class="sl-item__actions" @click.stop>
<button v-if="['COMPLETED','PROCESSING'].includes((item.status||'').toUpperCase())"
class="sl-icon-btn sl-icon-btn--view" @click="openStrategy(item)" title="Открыть">
class="sl-action-btn sl-action-btn--view" @click="openStrategy(item)" title="Открыть стратегию">
<i class="pi pi-eye"></i>
</button>
</div>
<!-- Arrow -->
<div v-if="['COMPLETED','PROCESSING','QUEUED'].includes((item.status||'').toUpperCase())" class="sl-item__arrow">
<i class="pi pi-angle-right"></i>
</div>
</div>
</TransitionGroup>
<!-- PAGINATION -->
<div v-if="filtered.length > pageSize" class="sl-pagination">
<span class="sl-pagination__info">Показано {{ (currentPage-1)*pageSize+1 }}{{ Math.min(currentPage*pageSize, filtered.length) }} из {{ filtered.length }}</span>
<Paginator :rows="pageSize" :totalRecords="filtered.length" :first="(currentPage-1)*pageSize" @page="currentPage=$event.page+1" template="PrevPageLink PageLinks NextPageLink" />
<span class="sl-pagination__info">
Показано {{ (currentPage-1)*pageSize+1 }}{{ Math.min(currentPage*pageSize, filtered.length) }} из {{ filtered.length }} стратегий
</span>
<Paginator :rows="pageSize" :totalRecords="filtered.length" :first="(currentPage-1)*pageSize"
@page="currentPage=$event.page+1" template="PrevPageLink PageLinks NextPageLink" />
</div>
</div>
@@ -253,78 +323,272 @@ onMounted(load);
</template>
<style scoped>
.sl-page { background: var(--kai-bg); min-height: 100vh; font-family: 'Onest', sans-serif; padding: 32px 24px 80px; }
.sl-inner { max-width: 1100px; margin: 0 auto; }
/* ──────────────────── PAGE ──────────────────── */
.sl-page {
background: #F5F8FF;
background-image:
radial-gradient(at 0% 0%, rgba(16,185,129,0.06) 0px, transparent 50%),
radial-gradient(at 100% 100%, rgba(45,123,255,0.06) 0px, transparent 50%);
min-height: 100vh;
font-family: 'Onest', sans-serif;
padding: 36px 24px 100px;
position: relative;
overflow-x: hidden;
}
.sl-glow {
position: absolute;
border-radius: 50%;
pointer-events: none;
z-index: 0;
}
.sl-glow--green { top: -150px; left: -100px; width: 500px; height: 500px; background: radial-gradient(circle, rgba(16,185,129,0.07) 0%, transparent 70%); }
.sl-glow--blue { bottom: -80px; right: -80px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(45,123,255,0.06) 0%, transparent 70%); }
.sl-inner { max-width: 1100px; margin: 0 auto; position: relative; z-index: 1; }
.sl-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; margin-bottom: 28px; flex-wrap: wrap; }
/* ──────────────────── HEADER ──────────────────── */
.sl-header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 16px;
margin-bottom: 32px;
flex-wrap: wrap;
}
.sl-header__left { display: flex; align-items: center; gap: 16px; }
.sl-back-btn { width: 36px; height: 36px; border-radius: 10px; border: 1px solid var(--kai-border); background: var(--kai-card); color: var(--kai-txt2); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; }
.sl-back-btn:hover { background: var(--kai-card-hover); color: var(--kai-txt); border-color: var(--kai-border-hover); }
.sl-eyebrow { font-size: 10px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.08em; color: var(--kai-green); margin-bottom: 4px; }
.sl-title { font-family: 'Unbounded', sans-serif; font-size: 24px; font-weight: 900; color: var(--kai-txt); }
.sl-btn { display: inline-flex; align-items: center; gap: 7px; padding: 9px 18px; border-radius: 10px; font-size: 13px; font-weight: 700; cursor: pointer; transition: all 0.2s; border: 1px solid; font-family: 'Onest', sans-serif; }
.sl-btn--primary { background: var(--kai-green); color: #0A0E1A; border-color: var(--kai-green); }
.sl-btn--primary:hover { opacity: 0.9; transform: translateY(-1px); }
.sl-btn--ghost { background: var(--kai-card); color: var(--kai-txt2); border-color: var(--kai-border); }
.sl-btn--ghost:hover { background: var(--kai-card-hover); color: var(--kai-txt); border-color: var(--kai-border-hover); }
.sl-back-btn {
width: 40px; height: 40px;
border-radius: 12px;
border: 1px solid rgba(16,185,129,0.22);
background: #fff;
color: #64748B;
display: flex; align-items: center; justify-content: center;
cursor: pointer;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(16,185,129,0.06);
flex-shrink: 0;
}
.sl-back-btn:hover { background: rgba(16,185,129,0.06); color: #10B981; border-color: rgba(16,185,129,0.4); transform: translateX(-2px); }
.sl-stats { display: flex; gap: 24px; margin-bottom: 24px; flex-wrap: wrap; }
.sl-stat { display: flex; flex-direction: column; gap: 2px; }
.sl-stat__num { font-family: 'Unbounded', sans-serif; font-size: 22px; font-weight: 900; line-height: 1; }
.sl-stat__label { font-size: 11px; color: var(--kai-txt3); font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; }
.sl-eyebrow {
display: flex; align-items: center; gap: 6px;
font-size: 10px; font-weight: 800; font-family: 'Unbounded', sans-serif;
text-transform: uppercase; letter-spacing: 0.1em; color: #10B981; margin-bottom: 5px;
}
.sl-eyebrow__dot {
width: 6px; height: 6px; border-radius: 50%; background: #10B981;
box-shadow: 0 0 8px rgba(16,185,129,0.5); animation: blink 2s infinite;
}
@keyframes blink { 0%,100%{opacity:1}50%{opacity:0.4} }
.sl-filters { display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; align-items: center; }
.sl-search { position: relative; flex: 1; min-width: 200px; max-width: 300px; }
.sl-search__icon { position: absolute; left: 12px; top: 50%; transform: translateY(-50%); color: var(--kai-txt3); font-size: 13px; }
.sl-search__input { width: 100%; padding: 9px 12px 9px 36px; border-radius: 10px; background: var(--kai-card); border: 1px solid var(--kai-border); color: var(--kai-txt); font-size: 13px; font-family: 'Onest', sans-serif; outline: none; transition: border-color 0.2s; }
.sl-search__input:focus { border-color: var(--kai-border-hover); }
.sl-search__input::placeholder { color: var(--kai-txt3); }
.sl-pills { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; }
.sl-pill { padding: 6px 14px; border-radius: 999px; font-size: 12px; font-weight: 600; border: 1px solid var(--kai-border); background: var(--kai-card); color: var(--kai-txt2); cursor: pointer; transition: all 0.2s; display: flex; align-items: center; gap: 5px; }
.sl-pill:hover { border-color: var(--kai-border-hover); color: var(--kai-txt); }
.sl-pill--active { background: rgba(0,212,139,0.1); border-color: rgba(0,212,139,0.4); color: var(--kai-green); }
.sl-pill__count { opacity: 0.7; font-size: 10px; }
.sl-icon-btn { width: 32px; height: 32px; border-radius: 8px; border: 1px solid var(--kai-border); background: var(--kai-card); color: var(--kai-txt2); display: flex; align-items: center; justify-content: center; cursor: pointer; transition: all 0.2s; font-size: 13px; }
.sl-icon-btn:hover { background: var(--kai-card-hover); color: var(--kai-txt); border-color: var(--kai-border-hover); }
.sl-icon-btn--view:hover { background: rgba(45,123,255,0.1); color: var(--kai-blue); border-color: rgba(45,123,255,0.3); }
.sl-title { font-family: 'Unbounded', sans-serif; font-size: 26px; font-weight: 900; color: #0F172A; letter-spacing: -0.02em; }
.sl-skeleton-list { display: flex; flex-direction: column; gap: 8px; }
.sl-skeleton-item { display: flex; gap: 14px; align-items: center; padding: 18px 20px; background: var(--kai-card); border: 1px solid var(--kai-border); border-radius: 14px; }
.sl-skeleton-icon { width: 44px; height: 44px; border-radius: 12px; background: rgba(255,255,255,0.06); flex-shrink: 0; }
.sl-skeleton-lines { flex: 1; display: flex; flex-direction: column; gap: 8px; }
.sl-skeleton-line { height: 12px; border-radius: 6px; background: rgba(255,255,255,0.06); }
.sl-skeleton-line--w40 { width: 40%; }
.sl-skeleton-line--w60 { width: 65%; }
/* ──────────────────── BUTTONS ──────────────────── */
.sl-btn {
display: inline-flex; align-items: center; gap: 7px;
padding: 10px 18px; border-radius: 12px;
font-size: 13px; font-weight: 700; cursor: pointer;
transition: all 0.22s; border: 1px solid; font-family: 'Onest', sans-serif; white-space: nowrap;
}
.sl-btn--primary { background: #10B981; color: #fff; border-color: #10B981; box-shadow: 0 4px 16px rgba(16,185,129,0.3); }
.sl-btn--primary:hover { background: #0EA572; box-shadow: 0 8px 28px rgba(16,185,129,0.4); transform: translateY(-2px); }
.sl-btn--ghost { background: #fff; color: #334155; border-color: rgba(16,185,129,0.2); box-shadow: 0 2px 8px rgba(16,185,129,0.05); }
.sl-btn--ghost:hover { background: rgba(16,185,129,0.05); color: #0F172A; border-color: rgba(16,185,129,0.35); }
.sl-empty { text-align: center; padding: 80px 24px; background: var(--kai-card); border: 1px solid var(--kai-border); border-radius: 20px; }
.sl-empty__icon { width: 72px; height: 72px; border-radius: 20px; background: rgba(0,212,139,0.1); border: 1px solid rgba(0,212,139,0.2); display: flex; align-items: center; justify-content: center; font-size: 30px; color: var(--kai-green); margin: 0 auto 20px; }
.sl-empty__title { font-family: 'Unbounded', sans-serif; font-size: 20px; font-weight: 700; color: var(--kai-txt); margin-bottom: 10px; }
.sl-empty__desc { font-size: 14px; color: var(--kai-txt2); max-width: 380px; margin: 0 auto 24px; line-height: 1.6; }
/* ──────────────────── STATS ──────────────────── */
.sl-stats-row { display: flex; gap: 12px; margin-bottom: 28px; flex-wrap: wrap; }
.sl-stat-card {
display: flex; align-items: center; gap: 12px;
padding: 14px 18px; background: #fff;
border: 1px solid rgba(16,185,129,0.14); border-radius: 14px;
flex: 1; min-width: 130px; transition: all 0.22s;
box-shadow: 0 2px 8px rgba(16,185,129,0.05);
}
.sl-stat-card:hover { transform: translateY(-3px); box-shadow: 0 8px 24px rgba(16,185,129,0.1); }
.sl-stat-card__icon {
width: 36px; height: 36px; border-radius: 10px;
display: flex; align-items: center; justify-content: center; font-size: 15px; flex-shrink: 0;
}
.sl-stat-card__body { display: flex; flex-direction: column; gap: 2px; }
.sl-stat-card__num { font-family: 'Unbounded', sans-serif; font-size: 20px; font-weight: 900; line-height: 1; }
.sl-stat-card__label { font-size: 10px; color: #94A3B8; font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; white-space: nowrap; }
.sl-list { display: flex; flex-direction: column; gap: 8px; }
.sl-item { display: flex; align-items: flex-start; gap: 16px; padding: 18px 20px; border-radius: 14px; background: var(--kai-card); border: 1px solid var(--kai-border); transition: all 0.25s; }
/* ──────────────────── TOOLBAR ──────────────────── */
.sl-toolbar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; flex-wrap: wrap; }
.sl-search { position: relative; flex: 1; min-width: 200px; max-width: 320px; }
.sl-search__icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: #94A3B8; font-size: 13px; pointer-events: none; }
.sl-search__input {
width: 100%; padding: 10px 36px 10px 40px; border-radius: 12px;
background: #fff; border: 1px solid rgba(16,185,129,0.18); color: #0F172A;
font-size: 13px; font-family: 'Onest', sans-serif; outline: none; transition: all 0.2s;
box-shadow: 0 2px 8px rgba(16,185,129,0.04);
}
.sl-search__input:focus { border-color: rgba(16,185,129,0.45); box-shadow: 0 0 0 3px rgba(16,185,129,0.08); }
.sl-search__input::placeholder { color: #94A3B8; }
.sl-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;
display: flex; align-items: center; justify-content: center;
cursor: pointer; font-size: 10px; transition: all 0.2s;
}
.sl-search__clear:hover { background: rgba(239,68,68,0.12); color: #EF4444; }
.sl-pills { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
.sl-pill {
display: inline-flex; align-items: center; gap: 5px;
padding: 7px 14px; border-radius: 999px; font-size: 12px; font-weight: 600;
border: 1px solid rgba(16,185,129,0.16); background: #fff; color: #64748B;
cursor: pointer; transition: all 0.2s; font-family: 'Onest', sans-serif;
}
.sl-pill:hover { border-color: rgba(16,185,129,0.35); color: #334155; background: rgba(16,185,129,0.04); }
.sl-pill--active { background: #10B981; border-color: #10B981; color: #fff; box-shadow: 0 4px 14px rgba(16,185,129,0.3); }
.sl-pill--active:hover { background: #0EA572; }
.sl-pill__badge {
background: rgba(255,255,255,0.25); border-radius: 999px; padding: 1px 6px;
font-size: 9px; font-weight: 800; font-family: 'Unbounded', sans-serif;
}
.sl-refresh-btn {
display: inline-flex; align-items: center; gap: 6px;
padding: 9px 14px; border-radius: 10px;
border: 1px solid rgba(16,185,129,0.18); background: #fff; color: #64748B;
font-size: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s;
font-family: 'Onest', sans-serif; white-space: nowrap;
}
.sl-refresh-btn:hover { background: rgba(16,185,129,0.05); color: #10B981; border-color: rgba(16,185,129,0.3); }
/* ──────────────────── SKELETON ──────────────────── */
.sl-skeleton-list { display: flex; flex-direction: column; gap: 10px; }
.sl-skeleton-item {
display: flex; gap: 16px; align-items: center;
padding: 20px 22px; background: #fff;
border: 1px solid rgba(16,185,129,0.1); border-radius: 16px;
}
.sl-skeleton-icon { width: 46px; height: 46px; border-radius: 13px; flex-shrink: 0; background: rgba(16,185,129,0.07); }
.sl-skeleton-lines { flex: 1; display: flex; flex-direction: column; gap: 9px; }
.sl-skeleton-line { height: 13px; border-radius: 7px; background: rgba(16,185,129,0.07); }
.sl-skeleton-badge { width: 80px; height: 24px; border-radius: 999px; background: rgba(16,185,129,0.07); }
.shimmer { position: relative; overflow: hidden; }
.shimmer::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(16,185,129,0.08), transparent);
animation: shimmer 1.6s infinite;
}
@keyframes shimmer { 0%{transform:translateX(-100%)} 100%{transform:translateX(100%)} }
/* ──────────────────── EMPTY ──────────────────── */
.sl-empty {
text-align: center; padding: 80px 24px; background: #fff;
border: 1px solid rgba(16,185,129,0.14); border-radius: 24px;
box-shadow: 0 4px 24px rgba(16,185,129,0.06);
}
.sl-empty__visual { position: relative; width: 80px; height: 80px; margin: 0 auto 24px; }
.sl-empty__ring {
position: absolute; inset: -6px; border-radius: 50%;
border: 2px solid rgba(16,185,129,0.15); animation: spin-slow 8s linear infinite;
}
@keyframes spin-slow { from{transform:rotate(0deg)} to{transform:rotate(360deg)} }
.sl-empty__icon {
width: 80px; height: 80px; border-radius: 22px;
background: rgba(16,185,129,0.09); border: 1px solid rgba(16,185,129,0.2);
display: flex; align-items: center; justify-content: center; font-size: 32px; color: #10B981;
}
.sl-empty__title { font-family: 'Unbounded', sans-serif; font-size: 20px; font-weight: 800; color: #0F172A; margin-bottom: 10px; }
.sl-empty__desc { font-size: 13.5px; color: #64748B; max-width: 400px; margin: 0 auto 28px; line-height: 1.65; }
.sl-empty__actions { display: flex; gap: 10px; justify-content: center; flex-wrap: wrap; }
/* ──────────────────── LIST ──────────────────── */
.sl-list { display: flex; flex-direction: column; gap: 10px; }
.sl-item {
display: flex; align-items: flex-start; gap: 16px;
padding: 18px 20px 18px 24px; border-radius: 16px; background: #fff;
border: 1px solid rgba(16,185,129,0.12); transition: all 0.25s cubic-bezier(0.4,0,0.2,1);
position: relative; overflow: hidden;
box-shadow: 0 2px 8px rgba(16,185,129,0.04);
}
.sl-item--clickable { cursor: pointer; }
.sl-item--clickable:hover { background: var(--kai-card-hover); border-color: var(--kai-border-hover); transform: translateY(-2px); box-shadow: 0 8px 32px rgba(0,0,0,0.3); }
.sl-item__status { width: 44px; height: 44px; border-radius: 12px; display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0; }
.sl-item__content { flex: 1; min-width: 0; }
.sl-item__row1 { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.sl-item__name { font-weight: 700; font-size: 14px; color: var(--kai-txt); line-height: 1.3; flex: 1; min-width: 0; }
.sl-item__badges { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; flex-shrink: 0; }
.sl-item__badge { padding: 2px 10px; border-radius: 999px; font-size: 10px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.05em; border: 1px solid; }
.sl-item__model { padding: 2px 10px; border-radius: 999px; font-size: 10px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.05em; border: 1px solid; }
.sl-item__meta { display: flex; gap: 14px; flex-wrap: wrap; font-size: 12px; color: var(--kai-txt3); }
.sl-item__meta i { margin-right: 3px; font-size: 11px; }
.sl-platforms { display: flex; align-items: center; gap: 6px; }
.sl-plat-chip { background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.1); padding: 1px 7px; border-radius: 6px; font-size: 10px; color: var(--kai-txt2); }
.sl-item__progress { margin-top: 10px; }
.sl-item__progress-msg { font-size: 11px; color: var(--kai-txt3); margin-bottom: 6px; display: block; }
.sl-item__progress-track { height: 3px; background: rgba(255,255,255,0.06); border-radius: 999px; overflow: hidden; }
.sl-item__progress-bar { height: 100%; background: linear-gradient(90deg, var(--kai-green), rgba(0,212,139,0.4)); border-radius: 999px; animation: sl-pulse 2s ease-in-out infinite; }
@keyframes sl-pulse { 0%,100%{opacity:1}50%{opacity:0.6} }
.sl-item__action { flex-shrink: 0; }
.sl-item--clickable:hover {
background: #FAFFFD; border-color: rgba(16,185,129,0.3);
transform: translateY(-3px); box-shadow: 0 12px 32px rgba(16,185,129,0.1);
}
.sl-pagination { display: flex; align-items: center; justify-content: space-between; padding-top: 16px; margin-top: 8px; flex-wrap: wrap; gap: 12px; }
.sl-pagination__info { font-size: 12px; color: var(--kai-txt3); }
.sl-item__stripe {
position: absolute; left: 0; top: 0; bottom: 0;
width: 4px; border-radius: 4px 0 0 4px; opacity: 0.7; transition: opacity 0.2s;
}
.sl-item--clickable:hover .sl-item__stripe { opacity: 1; }
.sl-item__status {
width: 46px; height: 46px; border-radius: 13px;
display: flex; align-items: center; justify-content: center; font-size: 18px; flex-shrink: 0;
transition: transform 0.2s; margin-top: 2px;
}
.sl-item--clickable:hover .sl-item__status { transform: scale(1.08); }
.sl-item__content { flex: 1; min-width: 0; }
.sl-item__top { display: flex; align-items: flex-start; gap: 10px; flex-wrap: wrap; margin-bottom: 8px; }
.sl-item__name { font-weight: 700; font-size: 14.5px; color: #0F172A; line-height: 1.3; flex: 1; min-width: 0; }
.sl-item__badges { display: flex; gap: 6px; align-items: center; flex-wrap: wrap; flex-shrink: 0; }
.sl-item__badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 10px; border-radius: 999px; font-size: 9.5px; font-weight: 800;
font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.06em; border: 1px solid;
}
.sl-item__badge i { font-size: 8px; }
.sl-item__model {
padding: 3px 10px; border-radius: 999px; font-size: 9.5px; font-weight: 800;
font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.05em; border: 1px solid;
}
.sl-item__meta { display: flex; gap: 7px; flex-wrap: wrap; align-items: center; }
.sl-meta-chip {
display: inline-flex; align-items: center; gap: 4px;
font-size: 11.5px; color: #64748B; font-weight: 500;
background: rgba(100,116,139,0.07); border: 1px solid rgba(100,116,139,0.12);
padding: 3px 9px; border-radius: 8px;
}
.sl-meta-chip i { font-size: 10px; color: #94A3B8; }
.sl-plat-chip {
display: inline-flex; align-items: center; gap: 4px;
padding: 3px 9px; border-radius: 8px;
font-size: 11px; font-weight: 600;
}
.sl-plat-chip i { font-size: 10px; }
.sl-item__progress { margin-top: 10px; display: flex; align-items: center; gap: 10px; }
.sl-item__progress-track {
flex: 1; height: 4px; background: rgba(16,185,129,0.1); border-radius: 999px; overflow: hidden;
}
.sl-item__progress-fill { height: 100%; border-radius: 999px; animation: sl-pulse 2.5s ease-in-out infinite; }
@keyframes sl-pulse { 0%,100%{opacity:1}50%{opacity:0.55} }
.sl-item__progress-msg { font-size: 10.5px; color: #94A3B8; font-weight: 600; white-space: nowrap; flex-shrink: 0; }
.sl-item__actions { flex-shrink: 0; display: flex; gap: 6px; align-items: center; }
.sl-action-btn {
width: 34px; height: 34px; border-radius: 10px;
border: 1px solid rgba(16,185,129,0.16); background: rgba(16,185,129,0.05);
color: #64748B; display: flex; align-items: center; justify-content: center;
cursor: pointer; transition: all 0.2s; font-size: 13px;
}
.sl-action-btn--view:hover { background: rgba(45,123,255,0.1); color: #2D7BFF; border-color: rgba(45,123,255,0.3); transform: scale(1.08); }
.sl-item__arrow {
width: 28px; height: 28px; border-radius: 8px;
display: flex; align-items: center; justify-content: center;
color: #CBD5E1; font-size: 14px; transition: all 0.2s; flex-shrink: 0;
}
.sl-item--clickable:hover .sl-item__arrow { color: #10B981; transform: translateX(3px); }
/* ──────────────────── PAGINATION ──────────────────── */
.sl-pagination {
display: flex; align-items: center; justify-content: space-between;
padding-top: 20px; margin-top: 8px; flex-wrap: wrap; gap: 12px;
border-top: 1px solid rgba(16,185,129,0.1);
}
.sl-pagination__info { font-size: 12px; color: #94A3B8; font-weight: 500; }
/* ──────────────────── TRANSITIONS ──────────────────── */
.sl-list-enter-active, .sl-list-leave-active { transition: all 0.25s ease; }
.sl-list-enter-from, .sl-list-leave-to { opacity: 0; transform: translateY(-6px); }
.sl-list-enter-from, .sl-list-leave-to { opacity: 0; transform: translateY(-8px); }
</style>
@@ -327,7 +327,7 @@ onMounted(() => {
</script>
<template>
<div class="mkt-hub app-dark">
<div class="td-campaign-page">
<Toast />
<!-- AMBIENT GLOW -->
@@ -465,24 +465,24 @@ onMounted(() => {
</TabPanel>
<TabPanel header="Аудитория" value="3">
<div class="grid gap-6 md:grid-cols-2 p-4">
<div class="mkt-card" style="background: rgba(255,255,255,0.02);">
<h3 class="wiz-label" style="opacity:0.7"><i class="pi pi-users"></i> Профиль аудитории</h3>
<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><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Сегмент</span> {{ audienceProfile?.primarySegment || '—' }}</div>
<div><span class="info-label">Сегмент</span> {{ audienceProfile?.primarySegment || '—' }}</div>
<div class="grid grid-cols-2 gap-4">
<div><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Возраст</span> {{ audienceProfile?.ageRange || '—' }}</div>
<div><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Гендер</span> {{ audienceProfile?.gender || '—' }}</div>
<div><span class="info-label">Возраст</span> {{ audienceProfile?.ageRange || '—' }}</div>
<div><span class="info-label">Гендер</span> {{ audienceProfile?.gender || '—' }}</div>
</div>
<div><span class="text-white opacity-40 uppercase text-[10px] block mb-1">География</span> {{ audienceProfile?.geography ? formatGeo(audienceProfile.geography) : '—' }}</div>
<div><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Интересы</span> {{ formatArrayField(audienceProfile?.interests ?? null) }}</div>
<div><span class="info-label">География</span> {{ audienceProfile?.geography ? formatGeo(audienceProfile.geography) : '—' }}</div>
<div><span class="info-label">Интересы</span> {{ formatArrayField(audienceProfile?.interests ?? null) }}</div>
</div>
</div>
<div class="mkt-card" style="background: rgba(255,255,255,0.02);">
<h3 class="wiz-label" style="opacity:0.7"><i class="pi pi-th-large"></i> Плейсменты и форматы</h3>
<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><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Рекумендованные площадки</span> {{ formatArrayField(recommendation?.recommendedPlacements ?? null) }}</div>
<div><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Форматы</span> {{ formatArrayField(recommendation?.recommendedAdFormats ?? null) }}</div>
<div><span class="text-white opacity-40 uppercase text-[10px] block mb-1">Тип таргетинга</span> {{ targetingTypeLabel(recommendation?.recommendedType ?? null) }}</div>
<div><span class="info-label">Рекомендованные площадки</span> {{ formatArrayField(recommendation?.recommendedPlacements ?? null) }}</div>
<div><span class="info-label">Форматы</span> {{ formatArrayField(recommendation?.recommendedAdFormats ?? null) }}</div>
<div><span class="info-label">Тип таргетинга</span> {{ targetingTypeLabel(recommendation?.recommendedType ?? null) }}</div>
</div>
</div>
</div>
@@ -499,71 +499,70 @@ onMounted(() => {
</template>
<style scoped>
:root {
--kai-bg: #0A0E1A;
--kai-card: rgba(255,255,255,0.04);
--kai-card-hover: rgba(255,255,255,0.07);
--kai-border: rgba(45,123,255,0.15);
--kai-border-hover: rgba(45,123,255,0.4);
--kai-blue: #2D7BFF;
--kai-green: #00D48B;
--kai-txt: rgba(255,255,255,0.92);
--kai-txt2: rgba(255,255,255,0.60);
--kai-txt3: rgba(255,255,255,0.35);
}
.mkt-hub {
background: #0A0E1A;
.td-campaign-page {
background: #F5F8FF;
min-height: 100vh;
font-family: 'Onest', sans-serif;
position: relative;
overflow: hidden;
padding: 48px 24px 80px;
color: var(--kai-txt);
color: #0F172A;
}
.mkt-hub__glow1 { position: absolute; top: -120px; right: -100px; width: 600px; height: 600px; background: radial-gradient(circle, rgba(45,123,255,0.08) 0%, transparent 70%); pointer-events: none; }
.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%); pointer-events: none; }
.info-label {
font-size: 9.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #64748B;
display: block;
margin-bottom: 3px;
}
.mkt-hub__glow1 { position: absolute; top: -120px; right: -100px; width: 600px; height: 600px; background: radial-gradient(circle, rgba(45,123,255,0.06) 0%, transparent 70%); pointer-events: none; }
.mkt-hub__glow2 { position: absolute; bottom: -80px; left: -60px; width: 400px; height: 400px; background: radial-gradient(circle, rgba(0,212,139,0.04) 0%, transparent 70%); pointer-events: none; }
.mkt-hub__inner { max-width: 1200px; margin: 0 auto; position: relative; z-index: 1; }
.mkt-hub__badge { display: inline-flex; align-items: center; gap: 8px; padding: 5px 14px; border-radius: 999px; border: 1px solid rgba(45,123,255,0.3); background: rgba(45,123,255,0.08); font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.07em; color: var(--kai-blue); margin-bottom: 20px; }
.mkt-hub__badge-dot { width: 6px; height: 6px; border-radius: 50%; background: var(--kai-blue); box-shadow: 0 0 8px var(--kai-blue); animation: pulse-dot 2s infinite; }
.mkt-hub__badge { display: inline-flex; align-items: center; gap: 8px; padding: 5px 14px; border-radius: 999px; border: 1px solid rgba(45,123,255,0.2); background: rgba(45,123,255,0.08); font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.07em; color: #2D7BFF; margin-bottom: 20px; }
.mkt-hub__badge-dot { width: 6px; height: 6px; border-radius: 50%; background: #2D7BFF; box-shadow: 0 0 8px #2D7BFF; animation: pulse-dot 2s infinite; }
@keyframes pulse-dot { 0%,100% { opacity:1; transform: scale(1); } 50% { opacity:0.5; transform: scale(0.8); } }
.mkt-hub__title { font-family: 'Unbounded', sans-serif; font-size: clamp(24px, 4vw, 36px); font-weight: 900; color: var(--kai-txt); letter-spacing: -0.02em; line-height: 1.1; }
.mkt-hub__title { font-family: 'Unbounded', sans-serif; font-size: clamp(24px, 4vw, 36px); font-weight: 900; color: #0F172A; letter-spacing: -0.02em; line-height: 1.1; }
.mkt-card { background: var(--kai-card); border: 1px solid var(--kai-border); border-radius: 20px; padding: 24px; transition: all 0.3s; position: relative; overflow: hidden; }
.mkt-card::before { content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity 0.3s; background: radial-gradient(circle at top right, rgba(45,123,255,0.1), transparent 60%); pointer-events: none; }
.mkt-card:hover { transform: translateY(-2px); border-color: var(--kai-border-hover); background: var(--kai-card-hover); }
.mkt-card { background: #FFFFFF; border: 1px solid rgba(45,123,255,0.18); border-radius: 20px; padding: 24px; transition: all 0.3s; position: relative; overflow: hidden; box-shadow: 0 2px 12px rgba(45,123,255,0.06); }
.mkt-card::before { content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity 0.3s; background: radial-gradient(circle at top right, rgba(45,123,255,0.06), transparent 60%); pointer-events: none; }
.mkt-card:hover { transform: translateY(-2px); border-color: rgba(45,123,255,0.35); background: #F8FBFF; box-shadow: 0 8px 24px rgba(45,123,255,0.1); }
.mkt-card:hover::before { opacity: 1; }
.td-back-mini { width: 36px; height: 36px; border-radius: 10px; border: 1px solid var(--kai-border); background: rgba(255,255,255,0.05); color: var(--kai-txt2); cursor: pointer; transition: all 0.2s; }
.td-back-mini:hover { color: #fff; background: var(--kai-blue); border-color: var(--kai-blue); }
.td-back-mini { width: 36px; height: 36px; border-radius: 10px; border: 1px solid rgba(45,123,255,0.18); background: rgba(45,123,255,0.05); color: #334155; cursor: pointer; transition: all 0.2s; display:flex; align-items:center; justify-content:center; }
.td-back-mini:hover { color: #fff; background: #2D7BFF; border-color: #2D7BFF; }
.td-campaign__status { display: inline-flex; align-items: center; gap: 8px; padding: 6px 16px; border-radius: 999px; border: 1px solid; font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.05em; }
.td-kpi-card { padding: 20px; text-align: center; }
.td-kpi-label { font-size: 10px; font-weight: 700; color: var(--kai-txt3); text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 8px; }
.td-kpi-val { font-family: 'Unbounded', sans-serif; font-size: 20px; font-weight: 900; color: #fff; }
.td-kpi-label { font-size: 10px; font-weight: 700; color: #64748B; text-transform: uppercase; letter-spacing: 0.1em; margin-bottom: 8px; }
.td-kpi-val { font-family: 'Unbounded', sans-serif; font-size: 20px; font-weight: 900; color: #0F172A; }
.wiz-pipe-node { flex: 1; padding: 12px; border-radius: 14px; border: 1px solid var(--kai-border); background: rgba(255,255,255,0.02); text-align: center; filter: grayscale(1); opacity: 0.4; transition: all 0.5s; }
.wiz-pipe-node.wiz-pipe--done { filter: grayscale(0); opacity: 1; border-color: var(--kai-green); background: rgba(0,212,139,0.05); color: var(--kai-green); }
.wiz-pipe-node.wiz-pipe--active { filter: grayscale(0); opacity: 1; border-color: var(--kai-blue); background: rgba(45,123,255,0.1); color: var(--kai-blue); box-shadow: 0 0 15px rgba(45,123,255,0.2); }
.wiz-pipe-node { flex: 1; padding: 12px; border-radius: 14px; border: 1px solid rgba(45,123,255,0.18); background: rgba(45,123,255,0.03); text-align: center; filter: grayscale(1); opacity: 0.5; transition: all 0.5s; color: #64748B; }
.wiz-pipe-node.wiz-pipe--done { filter: grayscale(0); opacity: 1; border-color: #10B981; background: rgba(16,185,129,0.08); color: #10B981; }
.wiz-pipe-node.wiz-pipe--active { filter: grayscale(0); opacity: 1; border-color: #2D7BFF; background: rgba(45,123,255,0.08); color: #2D7BFF; box-shadow: 0 0 15px rgba(45,123,255,0.15); }
.wiz-pipe-icon { font-size: 18px; margin-bottom: 4px; }
.wiz-pipe-label { font-size: 10px; font-weight: 700; text-transform: uppercase; }
.wiz-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.1em; color: var(--kai-txt3); }
.wiz-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.1em; color: #334155; }
.wiz-btn-sec { background: transparent; border: 1px solid var(--kai-border); color: var(--kai-txt2); padding: 12px 24px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.wiz-btn-sec:hover { background: var(--kai-card-hover); color: #fff; }
.wiz-btn-sec { background: transparent; border: 1px solid rgba(45,123,255,0.18); color: #334155; padding: 12px 24px; border-radius: 12px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.wiz-btn-sec:hover { background: rgba(45,123,255,0.06); color: #0F172A; }
.mkt-banner__cta { display: inline-flex; align-items: center; gap: 8px; padding: 12px 28px; border-radius: 12px; background: var(--kai-blue); color: #fff; font-size: 13px; font-weight: 700; font-family: 'Unbounded', sans-serif; transition: all 0.3s; cursor: pointer; }
.mkt-banner__cta { display: inline-flex; align-items: center; gap: 8px; padding: 12px 28px; border-radius: 12px; background: #2D7BFF; color: #fff; font-size: 13px; font-weight: 700; font-family: 'Unbounded', sans-serif; transition: all 0.3s; cursor: pointer; }
.mkt-banner__cta:hover { background: #1a6bff; box-shadow: 0 0 24px rgba(45,123,255,0.4); transform: translateY(-2px); }
:deep(.p-tabview-nav) { background: transparent !important; border: none !important; margin-bottom: 20px; }
:deep(.p-tabview-nav li .p-tabview-nav-link) { background: transparent !important; color: var(--kai-txt3) !important; border-bottom: 2px solid transparent; font-family: 'Unbounded', sans-serif; font-size: 12px; font-weight: 700; box-shadow: none !important; }
:deep(.p-tabview-nav li.p-highlight .p-tabview-nav-link) { color: var(--kai-blue) !important; border-color: var(--kai-blue) !important; }
:deep(.p-tabview-nav li .p-tabview-nav-link) { background: transparent !important; color: #64748B !important; border-bottom: 2px solid transparent; font-family: 'Unbounded', sans-serif; font-size: 12px; font-weight: 700; box-shadow: none !important; }
:deep(.p-tabview-nav li.p-highlight .p-tabview-nav-link) { color: #2D7BFF !important; border-color: #2D7BFF !important; }
:deep(.p-tabview-panels) { background: transparent !important; padding: 0 !important; }
@media (max-width: 768px) {
@@ -1,16 +1,13 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { useTargeting } from './composables/useTargeting';
import TargetingService from '@/service/TargetingService';
import MarketingV3Service from '@/service/MarketingV3Service';
import TargetingService from '@/service/TargetingService';
import { computed, onMounted, ref } from 'vue';
import { useRoute, useRouter } from 'vue-router';
import { useTargeting } from './composables/useTargeting';
import Button from 'primevue/button';
import InputNumber from 'primevue/inputnumber';
import InputText from 'primevue/inputtext';
import Checkbox from 'primevue/checkbox';
import InputSwitch from 'primevue/inputswitch';
import Dropdown from 'primevue/dropdown';
import InputNumber from 'primevue/inputnumber';
import InputSwitch from 'primevue/inputswitch';
import Toast from 'primevue/toast';
const router = useRouter();
@@ -262,10 +259,10 @@ const togglePlat = (val) => {
</Dropdown>
</div>
<div class="mkt-card" style="padding: 24px; background: rgba(45,123,255,0.05); border-color: rgba(45,123,255,0.2);">
<div class="mkt-card" style="padding: 24px; background: rgba(45,123,255,0.08); border-color: rgba(45,123,255,0.18);">
<div class="flex gap-4">
<i class="pi pi-info-circle" style="color: var(--kai-blue); font-size: 20px;"></i>
<p class="mkt-card__desc" style="color: var(--kai-txt);">ИИ автоматически проанализирует рынок и сегменты из стратегии для настройки точного таргетинга.</p>
<p class="mkt-card__desc" style="color: #334155;">ИИ автоматически проанализирует рынок и сегменты из стратегии для настройки точного таргетинга.</p>
</div>
</div>
</div>
@@ -385,27 +382,27 @@ const togglePlat = (val) => {
<style scoped>
:root {
--kai-bg: #0A0E1A;
--kai-card: rgba(255,255,255,0.04);
--kai-card-hover: rgba(255,255,255,0.07);
--kai-border: rgba(45,123,255,0.15);
--kai-border-hover: rgba(45,123,255,0.4);
--kai-bg: #F5F8FF;
--kai-card: #FFFFFF;
--kai-card-hover: #F8FBFF;
--kai-border: rgba(45,123,255,0.18);
--kai-border-hover: rgba(45,123,255,0.35);
--kai-blue: #2D7BFF;
--kai-green: #00D48B;
--kai-purple: #A855F7;
--kai-txt: rgba(255,255,255,0.92);
--kai-txt2: rgba(255,255,255,0.60);
--kai-txt3: rgba(255,255,255,0.35);
--kai-txt: #0F172A;
--kai-txt2: #334155;
--kai-txt3: #64748B;
}
.mkt-hub {
background: #0A0E1A;
background: #F5F8FF;
min-height: 100vh;
font-family: 'Onest', sans-serif;
position: relative;
overflow: hidden;
padding: 48px 24px 80px;
color: var(--kai-txt);
color: #0F172A;
}
.mkt-hub__glow1 {
@@ -440,7 +437,7 @@ const togglePlat = (val) => {
gap: 8px;
padding: 5px 14px;
border-radius: 999px;
border: 1px solid rgba(45,123,255,0.3);
border: 1px solid rgba(45,123,255,0.18);
background: rgba(45,123,255,0.08);
font-size: 11px;
font-weight: 700;
@@ -460,73 +457,74 @@ const togglePlat = (val) => {
}
@keyframes pulse-dot { 0%,100% { opacity:1; transform: scale(1); } 50% { opacity:0.5; transform: scale(0.8); } }
.mkt-hub__title { font-family: 'Unbounded', sans-serif; font-size: clamp(24px, 4vw, 42px); font-weight: 900; color: var(--kai-txt); letter-spacing: -0.02em; line-height: 1.1; }
.mkt-hub__sub { font-size: 15px; color: var(--kai-txt2); line-height: 1.6; font-weight: 400; }
.mkt-hub__title { font-family: 'Unbounded', sans-serif; font-size: clamp(24px, 4vw, 42px); font-weight: 900; color: #0F172A; letter-spacing: -0.02em; line-height: 1.1; }
.mkt-hub__sub { font-size: 15px; color: #334155; line-height: 1.6; font-weight: 400; }
.mkt-card {
background: var(--kai-card);
border: 1px solid var(--kai-border);
background: #FFFFFF;
border: 1px solid rgba(45,123,255,0.18);
border-radius: 20px;
padding: 24px;
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
position: relative;
overflow: hidden;
box-shadow: 0 2px 8px rgba(45,123,255,0.05);
}
.mkt-card::before { content: ''; position: absolute; inset: 0; opacity: 0; transition: opacity 0.3s; background: radial-gradient(circle at top right, rgba(45,123,255,0.1), transparent 60%); pointer-events: none; }
.mkt-card:hover { transform: translateY(-4px); border-color: var(--kai-border-hover); background: var(--kai-card-hover); }
.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); }
.mkt-card:hover::before { opacity: 1; }
.wiz-step-pill { width: 40px; height: 4px; border-radius: 2px; background: var(--kai-border); transition: all 0.4s; }
.wiz-step-pill { width: 40px; height: 4px; border-radius: 2px; background: rgba(45,123,255,0.18); transition: all 0.4s; }
.wiz-step-pill--active { background: var(--kai-blue); box-shadow: 0 0 10px var(--kai-blue); width: 60px; }
.wiz-container { width: 100%; animation: wiz-fade-in 0.6s ease-out; }
@keyframes wiz-fade-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: translateY(0); } }
.wiz-card { padding: 32px; }
.wiz-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.1em; color: var(--kai-txt3); margin-bottom: 12px; }
.wiz-label { display: flex; align-items: center; gap: 8px; font-size: 11px; font-weight: 700; font-family: 'Unbounded', sans-serif; text-transform: uppercase; letter-spacing: 0.1em; color: #64748B; margin-bottom: 12px; }
.wiz-dropdown :deep(.p-dropdown) { background: rgba(255,255,255,0.03); border: 1px solid var(--kai-border); border-radius: 12px; padding: 6px 12px; width: 100%; }
.wiz-dropdown :deep(.p-dropdown-label) { font-family: 'Onest', sans-serif; font-weight: 600; color: var(--kai-txt); }
.wiz-dropdown :deep(.p-dropdown) { background: #FFFFFF; border: 1px solid rgba(45,123,255,0.18); border-radius: 12px; padding: 6px 12px; width: 100%; }
.wiz-dropdown :deep(.p-dropdown-label) { font-family: 'Onest', sans-serif; font-weight: 600; color: #0F172A; }
.wiz-icon-mini { width: 32px; height: 32px; border-radius: 8px; background: rgba(45,123,255,0.15); color: var(--kai-blue); display: flex; align-items: center; justify-content: center; }
.wiz-input { width: 100%; background: rgba(255,255,255,0.03); border: 1px solid var(--kai-border); border-radius: 12px; padding: 14px 20px; color: #fff; font-family: 'Onest', sans-serif; font-size: 18px; font-weight: 600; outline: none; transition: all 0.3s; }
.wiz-input:focus { border-color: var(--kai-blue); background: rgba(255,255,255,0.06); box-shadow: 0 0 20px rgba(45,123,255,0.1); }
.wiz-input { width: 100%; background: #FFFFFF; border: 1px solid rgba(45,123,255,0.18); border-radius: 12px; padding: 14px 20px; color: #0F172A; font-family: 'Onest', sans-serif; font-size: 18px; font-weight: 600; outline: none; transition: all 0.3s; }
.wiz-input:focus { border-color: var(--kai-blue); background: #F8FBFF; box-shadow: 0 0 20px rgba(45,123,255,0.1); }
.wiz-input-num :deep(.p-inputtext) { background: transparent; border: none; font-family: 'Unbounded', sans-serif; font-size: 22px; font-weight: 900; color: #fff; padding: 0; }
.wiz-input-num :deep(.p-inputtext) { background: transparent; border: none; font-family: 'Unbounded', sans-serif; font-size: 22px; font-weight: 900; color: #0F172A; padding: 0; }
.wiz-platform { flex: 1; padding: 24px; background: rgba(255,255,255,0.03); border: 1px solid var(--kai-border); border-radius: 20px; display: flex; flex-direction: column; align-items: center; gap: 12px; cursor: pointer; transition: all 0.3s; position: relative; }
.wiz-platform:hover { border-color: var(--kai-border-hover); transform: translateY(-2px); }
.wiz-platform--active { background: rgba(45,123,255,0.1); border-color: var(--kai-blue); box-shadow: 0 0 20px rgba(45,123,255,0.15); }
.wiz-platform { flex: 1; padding: 24px; background: #FFFFFF; border: 1px solid rgba(45,123,255,0.18); border-radius: 20px; display: flex; flex-direction: column; align-items: center; gap: 12px; cursor: pointer; transition: all 0.3s; position: relative; box-shadow: 0 2px 8px rgba(45,123,255,0.05); }
.wiz-platform:hover { border-color: rgba(45,123,255,0.35); transform: translateY(-2px); box-shadow: 0 8px 20px rgba(45,123,255,0.1); }
.wiz-platform--active { background: rgba(45,123,255,0.08); border-color: var(--kai-blue); box-shadow: 0 0 20px rgba(45,123,255,0.15); }
.wiz-platform i { font-size: 32px; }
.wiz-check { position: absolute; top: 16px; right: 16px; width: 24px; height: 24px; border-radius: 50%; border: 2px solid var(--kai-border); display: flex; align-items: center; justify-content: center; }
.wiz-check { position: absolute; top: 16px; right: 16px; width: 24px; height: 24px; border-radius: 50%; border: 2px solid rgba(45,123,255,0.18); display: flex; align-items: center; justify-content: center; }
.wiz-platform--active .wiz-check { background: var(--kai-blue); border-color: var(--kai-blue); color: #fff; }
.wiz-toggle-card { display: flex; align-items: center; gap: 20px; padding: 24px; cursor: pointer; }
.wiz-toggle-card--active { border-color: var(--kai-blue); background: rgba(45,123,255,0.06); }
.wiz-toggle-card { display: flex; align-items: center; gap: 20px; padding: 24px; cursor: pointer; background: #FFFFFF; border: 1px solid rgba(45,123,255,0.18); box-shadow: 0 2px 8px rgba(45,123,255,0.05); }
.wiz-toggle-card--active { border-color: var(--kai-blue); background: rgba(45,123,255,0.06); box-shadow: 0 8px 20px rgba(45,123,255,0.1); }
.wiz-icon-main { width: 56px; height: 56px; border-radius: 16px; display: flex; align-items: center; justify-content: center; font-size: 24px; }
.wiz-toggle-title { font-family: 'Unbounded', sans-serif; font-size: 16px; font-weight: 700; color: var(--kai-txt); margin-bottom: 4px; }
.wiz-toggle-sub { font-size: 13px; color: var(--kai-txt3); }
.wiz-toggle-title { font-family: 'Unbounded', sans-serif; font-size: 16px; font-weight: 700; color: #0F172A; margin-bottom: 4px; }
.wiz-toggle-sub { font-size: 13px; color: #334155; }
.wiz-footer { margin-top: 60px; padding-top: 40px; border-top: 1px solid var(--kai-border); display: flex; justify-content: space-between; align-items: center; }
.wiz-footer { margin-top: 60px; padding-top: 40px; border-top: 1px solid rgba(45,123,255,0.18); display: flex; justify-content: space-between; align-items: center; }
.mkt-banner__cta { display: flex; align-items: center; gap: 8px; padding: 14px 32px; border-radius: 14px; background: var(--kai-blue); color: #fff; font-size: 14px; font-weight: 700; font-family: 'Unbounded', sans-serif; transition: all 0.3s; }
.mkt-banner__cta:hover { background: #1a6bff; box-shadow: 0 0 24px rgba(45,123,255,0.4); transform: translateY(-2px); }
.wiz-btn-sec { background: transparent; border: 1px solid var(--kai-border); color: var(--kai-txt2); padding: 12px 28px; border-radius: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.wiz-btn-sec:hover:not(:disabled) { background: var(--kai-card-hover); color: #fff; }
.wiz-btn-sec { background: transparent; border: 1px solid rgba(45,123,255,0.18); color: #334155; padding: 12px 28px; border-radius: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
.wiz-btn-sec:hover:not(:disabled) { background: #F8FBFF; color: #0F172A; }
.wiz-loading-view { flex: 1; display: flex; align-items: center; justify-content: center; padding: 40px 0; }
.wiz-loading-card { background: var(--kai-card); border: 1px solid var(--kai-border); border-radius: 40px; padding: 60px; width: 100%; max-width: 540px; position: relative; overflow: hidden; }
.wiz-loading-card { background: #FFFFFF; border: 1px solid rgba(45,123,255,0.18); border-radius: 40px; padding: 60px; width: 100%; max-width: 540px; position: relative; overflow: hidden; box-shadow: 0 8px 32px rgba(45,123,255,0.1); }
.wiz-loading-orb { position: absolute; top: -50px; right: -50px; width: 250px; height: 250px; background: radial-gradient(circle, rgba(45,123,255,0.15) 0%, transparent 70%); filter: blur(40px); }
.wiz-loading-icon { font-size: 48px; color: var(--kai-blue); margin-bottom: 24px; filter: drop-shadow(0 0 15px var(--kai-blue)); }
.wiz-loading-title { font-family: 'Unbounded', sans-serif; font-size: 28px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.1em; color: #fff; }
.wiz-loading-title { font-family: 'Unbounded', sans-serif; font-size: 28px; font-weight: 900; text-transform: uppercase; letter-spacing: 0.1em; color: #0F172A; }
.wiz-pipeline { position: relative; }
.wiz-pipeline-line { position: absolute; left: 17px; top: 10px; bottom: 10px; width: 2px; background: var(--kai-border); }
.wiz-pipeline-line { position: absolute; left: 17px; top: 10px; bottom: 10px; width: 2px; background: rgba(45,123,255,0.18); }
.wiz-pipeline-item { display: flex; gap: 20px; margin-bottom: 28px; opacity: 0.3; transition: all 0.5s; }
.wiz-pipeline-item--active { opacity: 1; }
.wiz-pipeline-dot { width: 36px; height: 36px; border-radius: 50%; background: var(--kai-bg); border: 2px solid var(--kai-border); display: flex; align-items: center; justify-content: center; z-index: 1; transition: all 0.5s; color: var(--kai-txt3); }
.wiz-pipeline-dot { width: 36px; height: 36px; border-radius: 50%; background: #FFFFFF; border: 2px solid rgba(45,123,255,0.18); display: flex; align-items: center; justify-content: center; z-index: 1; transition: all 0.5s; color: #64748B; }
.wiz-pipeline-item--active .wiz-pipeline-dot { border-color: var(--kai-blue); color: var(--kai-blue); box-shadow: 0 0 20px rgba(45,123,255,0.3); }
.animate-spin-slow { animation: spin 3s linear infinite; }
@@ -534,7 +532,7 @@ const togglePlat = (val) => {
.wiz-error { text-align: center; padding: 32px; background: rgba(239,68,68,0.1); border: 1px solid rgba(239,68,68,0.2); border-radius: 24px; color: #ef4444; }
.wiz-skeleton { background: rgba(255,255,255,0.03); border-radius: 20px; animation: pulse 2s infinite; }
.wiz-skeleton { background: rgba(45,123,255,0.06); border-radius: 20px; animation: pulse 2s infinite; }
@keyframes pulse { 0%, 100% { opacity: 0.5; } 50% { opacity: 0.3; } }
.fade-slide-enter-active, .fade-slide-leave-active { transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1); }
@@ -17,12 +17,12 @@ onMounted(async () => {
});
const STATUS_META = {
ACTIVE: { color: 'var(--kai-green)', bg: 'rgba(0,212,139,0.12)', border: 'rgba(0,212,139,0.3)', icon: 'pi pi-bolt', label: 'ACTIVE' },
PAUSED: { color: 'var(--kai-yellow)', bg: 'rgba(245,158,11,0.12)', border: 'rgba(245,158,11,0.3)', icon: 'pi pi-pause', label: 'PAUSED' },
FAILED: { color: 'var(--kai-red)', bg: 'rgba(239,68,68,0.12)', border: 'rgba(239,68,68,0.3)', icon: 'pi pi-exclamation-triangle', label: 'FAILED' },
GENERATING: { color: 'var(--kai-blue)', bg: 'rgba(45,123,255,0.12)', border: 'rgba(45,123,255,0.3)', icon: 'pi pi-spin pi-cog', label: 'GENERATING' },
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' },
};
const sm = s => STATUS_META[s] || { color: 'var(--kai-txt3)', bg: 'rgba(255,255,255,0.05)', border: 'rgba(255,255,255,0.1)', icon: 'pi pi-circle', label: s };
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 totalBudget = () => campaigns.value.reduce((sum, c) => sum + (c.budgetKzt || 0), 0);
const totalSpent = () => campaigns.value.reduce((sum, c) => sum + (c.spentKzt || 0), 0);
@@ -51,82 +51,96 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
</script>
<template>
<div class="mkt-hub">
<div class="td-page">
<Toast />
<!-- AMBIENT GLOW -->
<div class="mkt-hub__glow1"></div>
<div class="mkt-hub__glow2"></div>
<div class="td-glow td-glow--blue"></div>
<div class="td-glow td-glow--purple"></div>
<div class="mkt-hub__inner">
<div class="td-inner">
<!-- HEADER -->
<div class="mkt-hub__header" style="text-align: left; margin-bottom: 40px;">
<div class="flex items-center gap-4 mb-6">
<button class="td-back" @click="$router.push('/marketing-analysis')">
<div class="td-header">
<div class="td-header__top">
<button class="td-back-btn" @click="$router.push('/marketing-analysis')" title="Назад">
<i class="pi pi-arrow-left"></i>
</button>
<div>
<div class="mkt-hub__badge" style="margin-bottom: 0;">
<span class="mkt-hub__badge-dot"></span>
AI Targetologist · Beta
</div>
<div class="td-badge">
<span class="td-badge__dot"></span>
ИИ-Таргетолог · Бета
</div>
</div>
<div class="flex items-center justify-between gap-6 flex-wrap">
<div>
<h1 class="mkt-hub__title" style="margin-bottom: 8px;">Кабинет Таргетолога</h1>
<p class="mkt-hub__sub" style="margin: 0; text-align: left;">Управление рекламными кампаниями и AI-оптимизация бюджета</p>
<div class="td-header__main">
<div class="td-header__text">
<h1 class="td-title">Кабинет Таргетолога</h1>
<p class="td-subtitle">Управление рекламными кампаниями и ИИ-оптимизация бюджета</p>
</div>
<button class="td-launch-btn" @click="$router.push('/marketing-analysis/v3/targetologist/wizard')">
<i class="pi pi-sparkles"></i>
Создать AI кампанию
Создать ИИ-кампанию
</button>
</div>
</div>
<!-- KPI ROW -->
<div class="mkt-hub__grid" style="grid-template-columns: repeat(4, 1fr); margin-bottom: 40px;">
<div class="mkt-card">
<div class="mkt-card__icon" style="background:rgba(45,123,255,0.15);color:var(--kai-blue);">
<!-- 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;">
<i class="pi pi-list"></i>
</div>
<div class="td-kpi__num">{{ campaigns.length }}</div>
<div class="td-kpi__label">Кампаний всего</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>
<div class="mkt-card">
<div class="mkt-card__icon" style="background:rgba(0,212,139,0.15);color:var(--kai-green);">
<div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(16,185,129,0.12); color: #10B981;">
<i class="pi pi-bolt"></i>
</div>
<div class="td-kpi__num" style="color:var(--kai-green);">{{ activeCnt() }}</div>
<div class="td-kpi__label">Активно сейчас</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>
<div class="mkt-card">
<div class="mkt-card__icon" style="background:rgba(255,122,45,0.15);color:var(--kai-orange);">
<div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(249,115,22,0.12); color: #F97316;">
<i class="pi pi-dollar"></i>
</div>
<div class="td-kpi__num">{{ formatCurrency(totalSpent()) }}</div>
<div class="td-kpi__label">Всего потрачено</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>
<div class="mkt-card">
<div class="mkt-card__icon" style="background:rgba(168,85,247,0.15);color:var(--kai-purple);">
<div class="td-kpi-card">
<div class="td-kpi-card__icon" style="background: rgba(139,92,246,0.12); color: #8B5CF6;">
<i class="pi pi-wallet"></i>
</div>
<div class="td-kpi__num">{{ formatCurrency(totalBudget()) }}</div>
<div class="td-kpi__label">Общий лимит</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>
</div>
<!-- INTEGRATIONS -->
<div class="td-section-label">
<i class="pi pi-link" style="color:var(--kai-txt3)"></i>
Рекламные кабинеты
<div class="td-section-header">
<div class="td-section-label">
<i class="pi pi-link"></i>
Рекламные кабинеты
</div>
</div>
<div class="mkt-hub__grid" style="grid-template-columns: repeat(2, 1fr); margin-bottom: 40px;">
<div class="td-integrations-grid">
<OAuthConnectionWidget
platform="facebook"
title="Facebook & Instagram"
subtitle="Ads Manager API"
subtitle="API рекламного кабинета"
icon="pi pi-facebook"
themeClasses="bg-blue-500/10 blue"
:isConnected="state.accounts.facebook"
@@ -136,7 +150,7 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
<OAuthConnectionWidget
platform="tiktok"
title="TikTok for Business"
subtitle="Marketing API"
subtitle="API маркетинга"
icon="pi pi-tiktok"
themeClasses="bg-rose-500/10 rose"
:isConnected="state.accounts.tiktok"
@@ -145,87 +159,132 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
/>
</div>
<!-- CAMPAIGNS -->
<!-- CAMPAIGNS SECTION -->
<div class="td-campaigns-header">
<div class="td-section-label" style="margin-bottom:0;">
<i class="pi pi-chart-bar" style="color:var(--kai-txt3)"></i>
<div class="td-section-label">
<i class="pi pi-chart-bar"></i>
Список кампаний
</div>
<div class="td-search">
<i class="pi pi-search td-search__icon"></i>
<input v-model="searchQ" type="text" placeholder="Найти кампанию..." class="td-search__input" />
<button v-if="searchQ" class="td-search__clear" @click="searchQ = ''">
<i class="pi pi-times"></i>
</button>
</div>
</div>
<!-- SKELETON / EMPTY / LIST -->
<!-- SKELETON -->
<div v-if="loading" class="td-skeleton-list">
<div v-for="i in 3" :key="i" class="td-skeleton-item">
<div class="td-skeleton-bar"></div>
<div class="td-skeleton-stripe shimmer"></div>
<div class="td-skeleton-body">
<div class="td-skeleton-line shimmer" style="width: 35%; height: 14px; margin-bottom: 10px;"></div>
<div class="td-skeleton-line shimmer" style="width: 55%;"></div>
<div class="td-skeleton-metrics">
<div class="td-skeleton-metric shimmer"></div>
<div class="td-skeleton-metric shimmer"></div>
<div class="td-skeleton-metric shimmer"></div>
</div>
</div>
</div>
</div>
<div v-else-if="filteredCampaigns().length === 0" class="mkt-card" style="text-align: center; padding: 60px 24px;">
<div class="td-empty__icon"><i class="pi pi-chart-line"></i></div>
<h2 class="mkt-card__title" style="font-size: 20px;">Нет запущенных кампаний</h2>
<p class="mkt-card__desc" style="max-width: 420px; margin: 0 auto 24px;">ИИ-Таргетолог готов автоматизировать ваши расходы на рекламу, протестировать креативы и найти идеальную аудиторию.</p>
<button class="td-launch-btn" style="margin: 0 auto;" @click="$router.push('/marketing-analysis/v3/targetologist/wizard')">
<i class="pi pi-bolt"></i> Запустить первую кампанию
<!-- EMPTY -->
<div v-else-if="filteredCampaigns().length === 0" class="td-empty">
<div class="td-empty__visual">
<div class="td-empty__ring"></div>
<div class="td-empty__icon">
<i class="pi pi-chart-line"></i>
</div>
</div>
<h2 class="td-empty__title">Нет запущенных кампаний</h2>
<p class="td-empty__desc">ИИ-Таргетолог готов автоматизировать ваши расходы на рекламу, протестировать креативы и найти идеальную аудиторию.</p>
<button class="td-launch-btn" style="margin: 0 auto;"
@click="$router.push('/marketing-analysis/v3/targetologist/wizard')">
<i class="pi pi-bolt"></i>
Запустить первую кампанию
</button>
</div>
<!-- LIST -->
<div v-else class="td-campaign-list">
<div v-for="cmp in filteredCampaigns()" :key="cmp.id"
class="mkt-card td-campaign"
class="td-campaign"
@click="$router.push(`/marketing-analysis/v3/targetologist/campaign/${cmp.id}`)">
<div class="td-campaign__bar" :style="{ background: sm(cmp.status).color }"></div>
<!-- Left accent -->
<div class="td-campaign__stripe" :style="{ background: sm(cmp.status).accent }"></div>
<div class="td-campaign__body">
<!-- Top row -->
<div class="td-campaign__top">
<div class="td-campaign__info">
<h4 class="mkt-card__title" style="margin-bottom: 4px;">{{ cmp.name }}</h4>
<div class="td-campaign__name-row">
<h4 class="td-campaign__name">{{ cmp.name }}</h4>
<div class="td-campaign__status"
:style="{ color: sm(cmp.status).color, background: sm(cmp.status).bg, borderColor: sm(cmp.status).border }">
<i :class="sm(cmp.status).icon"></i>
{{ sm(cmp.status).label }}
</div>
</div>
<div class="td-campaign__meta">
<span><i class="pi pi-flag"></i> {{ cmp.goal }}</span>
<span>
<i v-if="cmp.platforms?.some(p=>p.includes('Insta'))" class="pi pi-instagram"></i>
<i v-if="cmp.platforms?.some(p=>p.includes('Face'))" class="pi pi-facebook"></i>
<i v-if="cmp.platforms?.some(p=>p.includes('Tik'))" class="pi pi-tiktok"></i>
<span v-if="cmp.goal">
<i class="pi pi-flag"></i>
{{ cmp.goal }}
</span>
<span v-if="cmp.platforms?.length" class="td-plat-chips">
<span v-if="cmp.platforms?.some(p => p.includes('Insta'))" class="td-plat-chip" style="color:#E1306C; background: rgba(225,48,108,0.08);">
<i class="pi pi-instagram"></i> Instagram
</span>
<span v-if="cmp.platforms?.some(p => p.includes('Face'))" class="td-plat-chip" style="color:#1877F2; background: rgba(24,119,242,0.08);">
<i class="pi pi-facebook"></i> Facebook
</span>
<span v-if="cmp.platforms?.some(p => p.includes('Tik'))" class="td-plat-chip" style="color:#69C9D0; background: rgba(105,201,208,0.08);">
<i class="pi pi-tiktok"></i> TikTok
</span>
</span>
</div>
</div>
<div class="td-campaign__status"
:style="{ color: sm(cmp.status).color, background: sm(cmp.status).bg, borderColor: sm(cmp.status).border }">
<i :class="sm(cmp.status).icon"></i>
{{ sm(cmp.status).label }}
</div>
</div>
<div class="td-campaign__bottom">
<!-- Metrics row -->
<div class="td-campaign__metrics">
<div class="td-metric">
<div class="td-metric__label">Потрачено</div>
<div class="td-metric__val">{{ formatCurrency(cmp.spentKzt || 0) }}</div>
<div class="td-metric__bar">
<div class="td-metric__bar-fill" :style="{ width: `${Math.min(((cmp.spentKzt||0)/(cmp.budgetKzt||1))*100,100)}%` }"></div>
<div class="td-metric__bar-fill"
:style="{ width: `${Math.min(((cmp.spentKzt||0)/(cmp.budgetKzt||1))*100,100)}%`, background: sm(cmp.status).accent }">
</div>
</div>
</div>
<div class="td-metric">
<div class="td-metric__label">CTR</div>
<div class="td-metric__val" style="color:var(--kai-green);">{{ cmp.ctr || '0.00' }}%</div>
<div class="td-metric__val" style="color: #10B981;">{{ cmp.ctr || '0.00' }}%</div>
</div>
<div class="td-metric">
<div class="td-metric__label">Охват</div>
<div class="td-metric__val">{{ new Intl.NumberFormat('ru-RU').format(cmp.reach || 0) }}</div>
</div>
<div class="td-metric">
<div class="td-metric__label">Бюджет</div>
<div class="td-metric__val" style="color: #8B5CF6;">{{ formatCurrency(cmp.budgetKzt || 0) }}</div>
</div>
<!-- Actions -->
<div class="td-campaign__actions" @click.stop>
<button v-if="cmp.status === 'ACTIVE'" class="td-act-btn td-act-btn--pause" @click="toggleStatus(cmp)" title="Пауза">
<button v-if="cmp.status === 'ACTIVE'" class="td-act-btn td-act-btn--pause"
@click="toggleStatus(cmp)" title="Поставить на паузу">
<i class="pi pi-pause"></i>
</button>
<button v-if="cmp.status === 'PAUSED'" class="td-act-btn td-act-btn--play" @click="toggleStatus(cmp)" title="Возобновить">
<button v-if="cmp.status === 'PAUSED'" class="td-act-btn td-act-btn--play"
@click="toggleStatus(cmp)" title="Возобновить кампанию">
<i class="pi pi-play"></i>
</button>
<button class="td-act-btn" @click="$router.push(`/marketing-analysis/v3/targetologist/campaign/${cmp.id}`)" title="Аналитика">
<button class="td-act-btn td-act-btn--open"
@click="$router.push(`/marketing-analysis/v3/targetologist/campaign/${cmp.id}`)"
title="Открыть аналитику">
<i class="pi pi-external-link"></i>
</button>
</div>
@@ -239,377 +298,294 @@ const filteredCampaigns = () => campaigns.value.filter(c =>
</template>
<style scoped>
/* ───── Page layout (from MarketingMain) ───── */
.mkt-hub {
background: var(--kai-bg);
/* ──────────────────── 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%);
min-height: 100vh;
font-family: 'Onest', sans-serif;
padding: 40px 24px 100px;
position: relative;
overflow: hidden;
padding: 48px 24px 80px;
overflow-x: hidden;
}
.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-inner { max-width: 1100px; margin: 0 auto; position: relative; z-index: 1; }
.mkt-hub__glow1 {
position: absolute;
top: -120px;
right: -100px;
width: 600px;
height: 600px;
background: radial-gradient(circle, rgba(45,123,255,0.08) 0%, transparent 70%);
pointer-events: none;
}
.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%);
pointer-events: none;
}
.mkt-hub__inner {
max-width: 1100px;
margin: 0 auto;
position: relative;
z-index: 1;
}
/* ───── Header (V4 Style) ───── */
.mkt-hub__badge {
display: inline-flex;
/* ──────────────────── HEADER ──────────────────── */
.td-header { margin-bottom: 40px; }
.td-header__top {
display: flex;
align-items: center;
gap: 8px;
padding: 5px 14px;
border-radius: 999px;
border: 1px solid rgba(45,123,255,0.3);
background: rgba(45,123,255,0.08);
font-size: 11px;
font-weight: 700;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--kai-blue);
gap: 16px;
margin-bottom: 20px;
}
.mkt-hub__badge-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background: var(--kai-blue);
box-shadow: 0 0 8px var(--kai-blue);
animation: pulse-dot 2s infinite;
}
@keyframes pulse-dot {
0%,100% { opacity:1; transform: scale(1); }
50% { opacity:0.5; transform: scale(0.8); }
.td-back-btn {
width: 40px; height: 40px; border-radius: 12px;
border: 1px solid rgba(45,123,255,0.2); 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;
}
.td-back-btn:hover { background: rgba(45,123,255,0.06); color: #2D7BFF; border-color: rgba(45,123,255,0.35); transform: translateX(-2px); }
.mkt-hub__title {
.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;
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} }
.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-size: clamp(24px, 4vw, 36px);
font-size: clamp(22px, 3.5vw, 32px);
font-weight: 900;
color: var(--kai-txt);
color: #0F172A;
letter-spacing: -0.02em;
line-height: 1.1;
line-height: 1.15;
margin-bottom: 8px;
}
.mkt-hub__sub {
font-size: 15px;
color: var(--kai-txt2);
line-height: 1.6;
font-weight: 400;
}
/* ───── Grid ───── */
.mkt-hub__grid {
display: grid;
gap: 16px;
}
@media (max-width: 768px) {
.mkt-hub__grid { grid-template-columns: 1fr !important; }
}
/* ───── Cards (V4 Style) ───── */
.mkt-card {
background: var(--kai-card);
border: 1px solid var(--kai-border);
border-radius: 20px;
padding: 24px;
transition: all 0.3s cubic-bezier(0.4,0,0.2,1);
position: relative;
overflow: hidden;
}
.mkt-card:hover {
transform: translateY(-4px);
border-color: var(--kai-border-hover);
background: var(--kai-card-hover);
box-shadow: 0 20px 60px rgba(0,0,0,0.4), 0 0 40px rgba(45,123,255,0.08);
}
.mkt-card__icon {
width: 48px;
height: 48px;
border-radius: 12px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
margin-bottom: 16px;
transition: transform 0.3s;
}
.mkt-card:hover .mkt-card__icon { transform: scale(1.1) rotate(3deg); }
.mkt-card__title {
font-family: 'Unbounded', sans-serif;
font-size: 16px;
font-weight: 700;
color: var(--kai-txt);
line-height: 1.25;
}
.mkt-card__desc {
font-size: 13px;
color: var(--kai-txt2);
line-height: 1.6;
font-weight: 400;
}
/* ───── Targetologist Specifics ───── */
.td-back {
width: 36px;
height: 36px;
border-radius: 10px;
border: 1px solid var(--kai-border);
background: var(--kai-card);
color: var(--kai-txt2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
}
.td-back:hover {
background: var(--kai-card-hover);
color: var(--kai-txt);
border-color: var(--kai-border-hover);
transform: translateX(-2px);
.td-subtitle {
font-size: 14px;
color: #64748B;
line-height: 1.55;
max-width: 480px;
}
.td-launch-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
border-radius: 14px;
background: var(--kai-blue);
color: #fff;
font-size: 13px;
font-weight: 700;
font-family: 'Unbounded', sans-serif;
border: none;
cursor: pointer;
transition: all 0.3s;
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;
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;
}
.td-launch-btn:hover {
background: #1a6bff;
box-shadow: 0 0 28px rgba(45,123,255,0.45);
transform: translateY(-2px);
.td-launch-btn:hover { background: #1a6bff; box-shadow: 0 10px 36px rgba(45,123,255,0.5); transform: translateY(-3px); }
/* ──────────────────── KPI GRID ──────────────────── */
.td-kpi-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 40px;
}
@media (max-width: 900px) { .td-kpi-grid { grid-template-columns: repeat(2, 1fr); } }
@media (max-width: 480px) { .td-kpi-grid { grid-template-columns: 1fr; } }
.td-kpi__num {
.td-kpi-card {
background: #fff;
border: 1px solid rgba(45,123,255,0.12);
border-radius: 18px;
padding: 22px 20px;
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;
}
.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;
}
.td-kpi-card:hover .td-kpi-card__icon { transform: scale(1.12) rotate(5deg); }
.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: var(--kai-txt);
line-height: 1;
margin-bottom: 6px;
font-size: 22px; font-weight: 900; color: #0F172A; line-height: 1; margin-bottom: 6px;
}
.td-kpi__label {
font-size: 11px;
color: var(--kai-txt3);
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
.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: 12px;
font-weight: 700;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--kai-txt2);
margin-bottom: 20px;
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;
}
.td-section-label i { font-size: 12px; }
/* ──────────────────── INTEGRATIONS ──────────────────── */
.td-integrations-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 14px;
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: 12px;
margin-bottom: 20px;
}
.td-search {
position: relative;
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: var(--kai-txt3);
font-size: 14px;
position: absolute; left: 14px; top: 50%; transform: translateY(-50%);
color: #94A3B8; font-size: 13px; pointer-events: none;
}
.td-search__input {
padding: 10px 16px 10px 44px;
border-radius: 12px;
background: var(--kai-card);
border: 1px solid var(--kai-border);
color: var(--kai-txt);
font-size: 14px;
font-family: 'Onest', sans-serif;
outline: none;
width: 280px;
transition: all 0.2s;
padding: 10px 38px 10px 42px; border-radius: 12px;
background: #fff; border: 1px solid rgba(45,123,255,0.16); 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);
}
.td-search__input:focus {
border-color: var(--kai-blue);
background: var(--kai-card-hover);
width: 320px;
.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::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;
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-campaign-list {
display: flex;
flex-direction: column;
gap: 12px;
/* ──────────────────── 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;
}
.td-skeleton-stripe { width: 5px; flex-shrink: 0; background: rgba(45,123,255,0.1); }
.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-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); }
.shimmer { position: relative; overflow: hidden; }
.shimmer::after {
content: ''; position: absolute; inset: 0;
background: linear-gradient(90deg, transparent, rgba(45,123,255,0.07), transparent);
animation: shimmer 1.6s infinite;
}
@keyframes shimmer { 0%{transform:translateX(-100%)} 100%{transform:translateX(100%)} }
/* ──────────────────── EMPTY ──────────────────── */
.td-empty {
text-align: center; padding: 80px 24px; background: #fff;
border: 1px solid rgba(45,123,255,0.14); 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;
}
@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);
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; }
/* ──────────────────── CAMPAIGN LIST ──────────────────── */
.td-campaign-list { display: flex; flex-direction: column; gap: 12px; }
.td-campaign {
padding: 0;
display: flex;
background: #fff;
border: 1px solid rgba(45,123,255,0.12);
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);
}
.td-campaign__bar {
width: 4px;
flex-shrink: 0;
.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__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__body {
flex: 1;
padding: 24px;
}
.td-campaign__top {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 16px;
margin-bottom: 20px;
}
.td-campaign__meta {
display: flex;
gap: 16px;
font-size: 12px;
color: var(--kai-txt3);
align-items: center;
.td-campaign__name {
font-family: 'Unbounded', sans-serif; font-size: 15px; font-weight: 700; color: #0F172A; line-height: 1.3; flex: 1;
}
.td-campaign__status {
display: inline-flex;
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;
}
.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 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 i { font-size: 10px; }
.td-campaign__metrics {
display: flex;
align-items: center;
gap: 6px;
padding: 4px 14px;
border-radius: 999px;
border: 1px solid;
font-size: 10px;
font-weight: 700;
font-family: 'Unbounded', sans-serif;
text-transform: uppercase;
letter-spacing: 0.06em;
gap: 28px;
flex-wrap: wrap;
}
.td-campaign__bottom {
display: flex;
align-items: center;
gap: 32px;
}
.td-metric {
display: flex;
flex-direction: column;
gap: 4px;
min-width: 120px;
}
.td-metric { display: flex; flex-direction: column; gap: 4px; min-width: 100px; }
.td-metric__label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--kai-txt3);
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: 18px;
font-weight: 900;
color: var(--kai-txt);
line-height: 1;
font-family: 'Unbounded', sans-serif; font-size: 17px; font-weight: 900; color: #0F172A; line-height: 1;
}
.td-metric__bar {
height: 4px;
background: rgba(255,255,255,0.06);
border-radius: 999px;
overflow: hidden;
margin-top: 6px;
width: 100%;
height: 4px; background: rgba(45,123,255,0.1); border-radius: 999px; overflow: hidden; margin-top: 4px;
}
.td-metric__bar-fill {
height: 100%;
background: var(--kai-blue);
border-radius: 999px;
transition: width 0.8s cubic-bezier(0.4,0,0.2,1);
height: 100%; border-radius: 999px; transition: width 0.9s cubic-bezier(0.4,0,0.2,1);
}
.td-campaign__actions {
display: flex;
gap: 8px;
margin-left: auto;
}
.td-campaign__actions { display: flex; gap: 8px; align-items: center; margin-left: auto; }
.td-act-btn {
width: 38px;
height: 38px;
border-radius: 10px;
border: 1px solid var(--kai-border);
background: rgba(255,255,255,0.04);
color: var(--kai-txt2);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.2s;
font-size: 14px;
width: 36px; height: 36px; border-radius: 10px;
border: 1px solid rgba(45,123,255,0.14); 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 {
background: var(--kai-card-hover);
color: var(--kai-txt);
border-color: var(--kai-border-hover);
transform: scale(1.05);
}
.td-empty__icon {
width: 72px;
height: 72px;
border-radius: 20px;
background: rgba(45,123,255,0.1);
border: 1px solid rgba(45,123,255,0.2);
display: flex;
align-items: center;
justify-content: center;
font-size: 30px;
color: var(--kai-blue);
margin: 0 auto 20px;
}
.td-skeleton-list { display: flex; flex-direction: column; gap: 12px; }
.td-skeleton-item { height: 140px; background: var(--kai-card); border: 1px solid var(--kai-border); border-radius: 20px; padding: 24px; }
.td-skeleton-bar { height: 20px; background: rgba(255,255,255,0.04); border-radius: 10px; width: 60%; }
.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); }
</style>