879 lines
27 KiB
HTML
879 lines
27 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>ROI24 — Интерактивный дашборд</title>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
|
||
<style>
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
:root {
|
||
--bg: #09090b;
|
||
--bg-card: #111113;
|
||
--bg-card-hover: #16161a;
|
||
--border: rgba(255,255,255,0.06);
|
||
--border-hover: rgba(255,255,255,0.1);
|
||
--text: #fafafa;
|
||
--text-sec: #a1a1aa;
|
||
--text-muted: #52525b;
|
||
--green: #22c55e;
|
||
--green-dim: rgba(34,197,94,0.12);
|
||
--blue: #3b82f6;
|
||
--blue-dim: rgba(59,130,246,0.12);
|
||
--amber: #f59e0b;
|
||
--amber-dim: rgba(245,158,11,0.12);
|
||
--red: #ef4444;
|
||
--red-dim: rgba(239,68,68,0.12);
|
||
--purple: #a855f7;
|
||
--purple-dim: rgba(168,85,247,0.12);
|
||
--cyan: #06b6d4;
|
||
--cyan-dim: rgba(6,182,212,0.12);
|
||
--mono: 'JetBrains Mono', monospace;
|
||
--ease: cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', -apple-system, sans-serif;
|
||
background: var(--bg);
|
||
color: var(--text);
|
||
line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
min-height: 100vh;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* Ambient glow */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
top: -200px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 600px;
|
||
height: 400px;
|
||
background: radial-gradient(ellipse, rgba(34,197,94,0.04) 0%, transparent 70%);
|
||
pointer-events: none;
|
||
}
|
||
|
||
/* ============================================
|
||
HEADER
|
||
============================================ */
|
||
.header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 28px;
|
||
background: rgba(17,17,19,0.85);
|
||
backdrop-filter: blur(16px);
|
||
border-bottom: 1px solid var(--border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.logo {
|
||
font-family: var(--mono);
|
||
font-weight: 700;
|
||
font-size: 15px;
|
||
letter-spacing: -0.3px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.logo-dot {
|
||
width: 8px; height: 8px;
|
||
background: var(--green);
|
||
border-radius: 50%;
|
||
animation: pulse 2s ease-in-out infinite;
|
||
box-shadow: 0 0 8px rgba(34,197,94,0.4);
|
||
}
|
||
|
||
@keyframes pulse {
|
||
0%, 100% { box-shadow: 0 0 8px rgba(34,197,94,0.4); }
|
||
50% { box-shadow: 0 0 16px rgba(34,197,94,0.15); }
|
||
}
|
||
|
||
.header-right {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 16px;
|
||
font-size: 12px;
|
||
}
|
||
|
||
.live-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
color: var(--green);
|
||
font-weight: 600;
|
||
}
|
||
|
||
.live-dot {
|
||
width: 6px; height: 6px;
|
||
background: var(--green);
|
||
border-radius: 50%;
|
||
animation: pulse 2s ease-in-out infinite;
|
||
}
|
||
|
||
.clock {
|
||
font-family: var(--mono);
|
||
color: var(--text-muted);
|
||
font-size: 11px;
|
||
}
|
||
|
||
/* ============================================
|
||
SEGMENT SWITCHER
|
||
============================================ */
|
||
.switcher-wrap {
|
||
display: flex;
|
||
justify-content: center;
|
||
padding: 32px 24px 0;
|
||
}
|
||
|
||
.switcher {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 5px;
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
}
|
||
|
||
.sw-btn {
|
||
padding: 10px 20px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--text-sec);
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
font-weight: 500;
|
||
border-radius: 10px;
|
||
cursor: pointer;
|
||
transition: all 0.25s var(--ease);
|
||
white-space: nowrap;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
}
|
||
|
||
.sw-btn:hover { color: var(--text); }
|
||
|
||
.sw-btn.active {
|
||
background: var(--green-dim);
|
||
color: var(--green);
|
||
box-shadow: 0 0 0 1px rgba(34,197,94,0.2);
|
||
}
|
||
|
||
.sw-btn .icon { font-size: 15px; }
|
||
|
||
/* ============================================
|
||
KPI GRID — 6 cards
|
||
============================================ */
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(6, 1fr);
|
||
gap: 12px;
|
||
padding: 24px 24px 0;
|
||
max-width: 1280px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.kpi {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: all 0.25s var(--ease);
|
||
opacity: 0;
|
||
transform: translateY(12px);
|
||
}
|
||
|
||
.kpi.visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.kpi:hover {
|
||
border-color: var(--border-hover);
|
||
transform: translateY(-2px);
|
||
}
|
||
|
||
.kpi-accent {
|
||
position: absolute;
|
||
top: 0; left: 0;
|
||
width: 3px; height: 100%;
|
||
opacity: 0;
|
||
transition: opacity 0.25s;
|
||
}
|
||
|
||
.kpi:hover .kpi-accent { opacity: 1; }
|
||
|
||
.kpi-name {
|
||
font-size: 10px;
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
color: var(--text-muted);
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.kpi-val {
|
||
font-family: var(--mono);
|
||
font-size: 22px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.kpi-meta {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 6px;
|
||
font-size: 11px;
|
||
}
|
||
|
||
.kpi-trend {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 3px;
|
||
padding: 2px 6px;
|
||
border-radius: 5px;
|
||
font-family: var(--mono);
|
||
font-weight: 600;
|
||
font-size: 10px;
|
||
}
|
||
|
||
.kpi-trend.up { color: var(--green); background: var(--green-dim); }
|
||
.kpi-trend.down { color: var(--red); background: var(--red-dim); }
|
||
.kpi-trend.neutral { color: var(--blue); background: var(--blue-dim); }
|
||
|
||
.kpi-period {
|
||
color: var(--text-muted);
|
||
font-size: 10px;
|
||
}
|
||
|
||
/* ============================================
|
||
CHART ROWS
|
||
============================================ */
|
||
.chart-row {
|
||
display: grid;
|
||
gap: 12px;
|
||
padding: 16px 24px;
|
||
max-width: 1280px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.chart-row.two { grid-template-columns: 2fr 1fr; }
|
||
.chart-row.three { grid-template-columns: 1fr 1fr 1fr; }
|
||
|
||
.chart-card {
|
||
background: var(--bg-card);
|
||
border: 1px solid var(--border);
|
||
border-radius: 14px;
|
||
padding: 20px;
|
||
transition: border-color 0.25s;
|
||
}
|
||
|
||
.chart-card:hover { border-color: var(--border-hover); }
|
||
|
||
.chart-head {
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.chart-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
}
|
||
|
||
.chart-sub {
|
||
font-size: 11px;
|
||
color: var(--text-muted);
|
||
}
|
||
|
||
.chart-dom {
|
||
width: 100%;
|
||
height: 280px;
|
||
}
|
||
|
||
.chart-dom.small { height: 200px; }
|
||
|
||
/* ============================================
|
||
METRIC LIST
|
||
============================================ */
|
||
.metric-list {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 10px;
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.metric-item {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--border);
|
||
}
|
||
|
||
.metric-item:last-child { border-bottom: none; }
|
||
|
||
.metric-label {
|
||
font-size: 12px;
|
||
color: var(--text-sec);
|
||
flex: 1;
|
||
}
|
||
|
||
.metric-val {
|
||
font-family: var(--mono);
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
color: var(--text);
|
||
margin: 0 12px;
|
||
min-width: 60px;
|
||
text-align: right;
|
||
}
|
||
|
||
.metric-bar {
|
||
width: 70px; height: 4px;
|
||
background: var(--border);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.metric-fill {
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
width: 0;
|
||
transition: width 1s var(--ease);
|
||
}
|
||
|
||
/* ============================================
|
||
SECTION HEADER
|
||
============================================ */
|
||
.section-header {
|
||
text-align: center;
|
||
padding: 40px 24px 20px;
|
||
}
|
||
|
||
.section-header h2 {
|
||
font-size: clamp(1.6rem, 4vw, 2.2rem);
|
||
font-weight: 800;
|
||
letter-spacing: -0.5px;
|
||
margin-bottom: 6px;
|
||
}
|
||
|
||
.section-header p {
|
||
color: var(--text-sec);
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ============================================
|
||
RESPONSIVE
|
||
============================================ */
|
||
@media (max-width: 1024px) {
|
||
.kpi-grid { grid-template-columns: repeat(3, 1fr); }
|
||
.chart-row.two,
|
||
.chart-row.three { grid-template-columns: 1fr; }
|
||
}
|
||
|
||
@media (max-width: 640px) {
|
||
.kpi-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.sw-btn { padding: 8px 12px; font-size: 11px; }
|
||
.sw-btn .label { display: none; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- HEADER -->
|
||
<div class="header">
|
||
<div class="logo">
|
||
<span class="logo-dot"></span>
|
||
ROI24
|
||
</div>
|
||
<div class="header-right">
|
||
<div class="live-badge">
|
||
<span class="live-dot"></span>
|
||
Live
|
||
</div>
|
||
<div class="clock" id="clock">--:--:--</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- SECTION HEADER -->
|
||
<div class="section-header">
|
||
<h2>Все ключевые цифры — в одном месте</h2>
|
||
<p>Переключите бизнес, чтобы увидеть свой дашборд</p>
|
||
</div>
|
||
|
||
<!-- SWITCHER -->
|
||
<div class="switcher-wrap">
|
||
<div class="switcher" id="switcher">
|
||
<button class="sw-btn active" data-seg="ecom">
|
||
<span class="icon">🛒</span>
|
||
<span class="label">Интернет-магазин</span>
|
||
</button>
|
||
<button class="sw-btn" data-seg="services">
|
||
<span class="icon">💼</span>
|
||
<span class="label">Услуги / Агентство</span>
|
||
</button>
|
||
<button class="sw-btn" data-seg="production">
|
||
<span class="icon">🏭</span>
|
||
<span class="label">Производство</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- DASHBOARD CONTAINER -->
|
||
<div id="dash">
|
||
<!-- KPI Row -->
|
||
<div class="kpi-grid" id="kpi-grid"></div>
|
||
|
||
<!-- Row 1: Main + Side -->
|
||
<div class="chart-row two">
|
||
<div class="chart-card">
|
||
<div class="chart-head">
|
||
<div class="chart-title" id="mc-title">Выручка</div>
|
||
<div class="chart-sub" id="mc-sub">12 месяцев · с трендом</div>
|
||
</div>
|
||
<div class="chart-dom" id="mc"></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-head">
|
||
<div class="chart-title" id="sc-title">ROI по каналам</div>
|
||
<div class="chart-sub">Текущий период</div>
|
||
</div>
|
||
<div class="chart-dom" id="sc"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Row 2: Three small -->
|
||
<div class="chart-row three">
|
||
<div class="chart-card">
|
||
<div class="chart-head">
|
||
<div class="chart-title">ROMI рекламных кампаний</div>
|
||
<div class="chart-sub">Возврат инвестиций в маркетинг</div>
|
||
</div>
|
||
<div class="chart-dom small" id="ch1"></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-head">
|
||
<div class="chart-title">Воронка продаж</div>
|
||
<div class="chart-sub">Конверсия по этапам</div>
|
||
</div>
|
||
<div class="chart-dom small" id="ch2"></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-head">
|
||
<div class="chart-title">Выполнение плана</div>
|
||
<div class="chart-sub">Ключевые метрики</div>
|
||
</div>
|
||
<div class="metric-list" id="mlist"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ECharts -->
|
||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||
<script>
|
||
// ============================================
|
||
// DATA
|
||
// ============================================
|
||
const D = {
|
||
ecom: {
|
||
kpis: [
|
||
{ name: 'Выручка', val: '4.2М ₽', trend: 12, trendTxt: '+12%', per: 'vs прошл. мес' },
|
||
{ name: 'CPC', val: '42 ₽', trend: -15, trendTxt: '-15%', per: 'оптимизация' },
|
||
{ name: 'Конверсия', val: '3.8%', trend: 0.5, trendTxt: '+0.5%', per: 'от клика до оплаты' },
|
||
{ name: 'CAC', val: '680 ₽', trend: -8, trendTxt: '-8%', per: 'стоимость клиента' },
|
||
{ name: 'LTV', val: '12 400 ₽', trend: 18, trendTxt: '+18%', per: 'за 6 мес' },
|
||
{ name: 'LTV/CAC', val: '18.2x', trend: 28, trendTxt: '+28%', per: 'коэф. окупаемости' },
|
||
],
|
||
mainChart: {
|
||
title: 'Выручка и реклама',
|
||
sub: '12 месяцев · выручка (зелёный) vs расход (синий)',
|
||
type: 'dual',
|
||
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
revenue: [2.8,3.1,2.9,3.4,3.6,3.2,3.8,4.0,3.9,4.2,3.7,4.2],
|
||
adSpend: [0.6,0.7,0.65,0.72,0.68,0.75,0.7,0.65,0.6,0.58,0.62,0.55],
|
||
},
|
||
sideChart: {
|
||
title: 'ROI по каналам',
|
||
type: 'bar-h',
|
||
data: [
|
||
{ name: 'SEO', val: 340 },
|
||
{ name: 'Email', val: 280 },
|
||
{ name: 'Прямой', val: 210 },
|
||
{ name: 'Контекст', val: 150 },
|
||
{ name: 'Соцсети', val: 120 },
|
||
{ name: 'Рефералы', val: 95 },
|
||
],
|
||
},
|
||
romiChart: {
|
||
type: 'bar-group',
|
||
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
romi: [180,210,195,240,260,220,280,310,290,320,270,340],
|
||
},
|
||
funnel: {
|
||
data: [
|
||
{ name: 'Посетители', value: 45000 },
|
||
{ name: 'Корзина', value: 8100 },
|
||
{ name: 'Оформление', value: 4950 },
|
||
{ name: 'Оплата', value: 2723 },
|
||
],
|
||
},
|
||
metrics: [
|
||
{ label: 'Выручка (план)', value: '4.2М', max: '4.5М', pct: 93, color: 'var(--green)' },
|
||
{ label: 'Конверсия (план)', value: '3.8%', max: '4.0%', pct: 95, color: 'var(--green)' },
|
||
{ label: 'CAC (план)', value: '680₽', max: '600₽', pct: 88, color: 'var(--amber)' },
|
||
{ label: 'LTV (план)', value: '12.4К', max: '12К', pct: 104, color: 'var(--green)' },
|
||
{ label: 'ROMI (план)', value: '290%', max: '250%', pct: 116, color: 'var(--green)' },
|
||
],
|
||
},
|
||
services: {
|
||
kpis: [
|
||
{ name: 'Пайплайн', val: '12.8М ₽', trend: 18, trendTxt: '+18%', per: 'к прошлому мес' },
|
||
{ name: 'CPC', val: '85 ₽', trend: -10, trendTxt: '-10%', per: 'контекст' },
|
||
{ name: 'Конверсия', val: '24%', trend: 3, trendTxt: '+3%', per: 'лид→сделка' },
|
||
{ name: 'CAC', val: '4 200 ₽', trend: -12, trendTxt: '-12%', per: 'стоимость лида' },
|
||
{ name: 'LTV', val: '89 000 ₽', trend: 22, trendTxt: '+22%', per: 'за 12 мес' },
|
||
{ name: 'ROMI', val: '245%', trend: 35, trendTxt: '+35%', per: 'маркетинг ROI' },
|
||
],
|
||
mainChart: {
|
||
title: 'Пайплайн и доход',
|
||
sub: '12 месяцев · пайплайн (синий) vs закрытие (зелёный)',
|
||
type: 'dual',
|
||
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
pipeline: [6.2,7.0,7.5,8.1,8.8,9.2,9.8,10.5,11.0,11.5,12.2,12.8],
|
||
closed: [1.8,2.1,2.0,2.3,2.5,2.2,2.8,3.0,2.7,3.1,2.6,3.2],
|
||
},
|
||
sideChart: {
|
||
title: 'ROMI по услугам',
|
||
type: 'bar-h',
|
||
data: [
|
||
{ name: 'SEO-контент', val: 420 },
|
||
{ name: 'Рефералы', val: 380 },
|
||
{ name: 'Консалтинг', val: 290 },
|
||
{ name: 'Дизайн', val: 240 },
|
||
{ name: 'Таргет', val: 160 },
|
||
{ name: 'Холодные', val: 85 },
|
||
],
|
||
},
|
||
romiChart: {
|
||
type: 'bar-group',
|
||
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
romi: [120,145,160,180,200,190,220,250,235,260,215,280],
|
||
},
|
||
funnel: {
|
||
data: [
|
||
{ name: 'Лиды', value: 320 },
|
||
{ name: 'Квалификация', value: 224 },
|
||
{ name: 'Предложение', value: 128 },
|
||
{ name: 'Сделка', value: 77 },
|
||
],
|
||
},
|
||
metrics: [
|
||
{ label: 'Пайплайн (план)', value: '12.8М', max: '15М', pct: 85, color: 'var(--blue)' },
|
||
{ label: 'Конверсия (план)', value: '24%', max: '25%', pct: 96, color: 'var(--green)' },
|
||
{ label: 'CAC (план)', value: '4.2К', max: '4К', pct: 95, color: 'var(--green)' },
|
||
{ label: 'ROMI (план)', value: '245%', max: '200%', pct: 123, color: 'var(--green)' },
|
||
{ label: 'Загрузка (план)', value: '87%', max: '90%', pct: 97, color: 'var(--green)' },
|
||
],
|
||
},
|
||
production: {
|
||
kpis: [
|
||
{ name: 'Выпуск', val: '15 420 шт', trend: 8, trendTxt: '+8%', per: 'к плану' },
|
||
{ name: 'CPC (трафик)', val: '18 ₽', trend: -20, trendTxt: '-20%', per: 'SEO оптимизация' },
|
||
{ name: 'OEE', val: '78%', trend: 4, trendTxt: '+4%', per: 'загрузка мощностей' },
|
||
{ name: 'Брак', val: '2.1%', trend: -0.5, trendTxt: '-0.5%', per: 'снижение потерь' },
|
||
{ name: 'Себестоимость', val: '3 200 ₽', trend: -3, trendTxt: '-3%', per: 'экономия сырья' },
|
||
{ name: 'ROMI рекламы', val: '310%', trend: 45, trendTxt: '+45%', per: 'контекст + SEO' },
|
||
],
|
||
mainChart: {
|
||
title: 'План/факт выпуска',
|
||
sub: '12 месяцев · план (пунктир) vs факт (сплошная)',
|
||
type: 'bar-compare',
|
||
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
plan: [12,13,12.5,14,14.5,13,15,15.5,15,15.5,14,16],
|
||
fact: [11.5,12.8,12.2,13.8,14.2,12.5,14.8,15.2,14.7,15.0,13.8,15.4],
|
||
},
|
||
sideChart: {
|
||
title: 'ROMI по каналам',
|
||
type: 'bar-h',
|
||
data: [
|
||
{ name: 'SEO', val: 380 },
|
||
{ name: 'Прямые', val: 310 },
|
||
{ name: 'Контекст', val: 210 },
|
||
{ name: 'Каталоги', val: 170 },
|
||
{ name: 'Таргет', val: 140 },
|
||
{ name: 'Выставки', val: 110 },
|
||
],
|
||
},
|
||
romiChart: {
|
||
type: 'bar-group',
|
||
labels: ['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||
romi: [180,200,220,250,280,260,290,310,300,330,290,350],
|
||
},
|
||
funnel: {
|
||
data: [
|
||
{ name: 'Заказ', value: 15420 },
|
||
{ name: 'Контроль', value: 15200 },
|
||
{ name: 'Брак', value: 324 },
|
||
{ name: 'Доработка', value: 180 },
|
||
],
|
||
},
|
||
metrics: [
|
||
{ label: 'Выпуск (план)', value: '15.4К', max: '16К', pct: 96, color: 'var(--green)' },
|
||
{ label: 'OEE (план)', value: '78%', max: '85%', pct: 92, color: 'var(--amber)' },
|
||
{ label: 'Брак (план)', value: '2.1%', max: '2.0%', pct: 95, color: 'var(--amber)' },
|
||
{ label: 'ROMI (план)', value: '310%', max: '280%', pct: 111, color: 'var(--green)' },
|
||
{ label: 'Себест. (план)', value: '3.2К', max: '3.0К', pct: 94, color: 'var(--blue)' },
|
||
],
|
||
},
|
||
};
|
||
|
||
let charts = {};
|
||
let current = 'ecom';
|
||
|
||
// ============================================
|
||
// RENDER KPIs
|
||
// ============================================
|
||
function renderKpis(seg) {
|
||
const g = document.getElementById('kpi-grid');
|
||
const k = seg.kpis;
|
||
g.innerHTML = k.map((d, i) => {
|
||
const tClass = d.trend > 0 ? 'up' : (d.trend < 0 && i === 0 ? 'up' : 'neutral');
|
||
const dir = d.trend > 0 ? '↑' : (d.trend < 0 && i === 0 ? '↓' : '→');
|
||
return `<div class="kpi" style="transition-delay:${i*80}ms">
|
||
<div class="kpi-accent" style="background:${i<3?'var(--green)':i<5?'var(--blue)':'var(--purple)'}"></div>
|
||
<div class="kpi-name">${d.name}</div>
|
||
<div class="kpi-val">${d.val}</div>
|
||
<div class="kpi-meta">
|
||
<span class="kpi-trend ${tClass}">${dir} ${d.trendTxt}</span>
|
||
<span class="kpi-period">${d.per}</span>
|
||
</div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
requestAnimationFrame(() => {
|
||
g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible'));
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// MAIN CHART
|
||
// ============================================
|
||
function renderMain(seg) {
|
||
const c = seg.mainChart;
|
||
const el = document.getElementById('mc');
|
||
if (!charts.mc) charts.mc = echarts.init(el);
|
||
document.getElementById('mc-title').textContent = c.title;
|
||
document.getElementById('mc-sub').textContent = c.sub;
|
||
|
||
let opt;
|
||
if (c.type === 'dual') {
|
||
opt = {
|
||
tooltip: { trigger:'axis', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
|
||
legend: { data:['Доход','Расход'], textStyle:{color:'#52525b',fontSize:10}, top:0 },
|
||
grid: { left:44, right:16, top:28, bottom:28 },
|
||
xAxis: { type:'category', data:c.labels, axisLine:{lineStyle:{color:'#1e1e22'}}, axisLabel:{color:'#52525b',fontSize:10} },
|
||
yAxis: { type:'value', splitLine:{lineStyle:{color:'#1a1a1e'}}, axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}М'} },
|
||
series: [
|
||
{ name:'Доход', type:'line', data:c.pipeline||c.revenue, smooth:0.4, symbol:'none', lineStyle:{width:2.5,color:'#22c55e'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(34,197,94,0.2)'},{offset:1,color:'rgba(34,197,94,0.01)'}])} },
|
||
{ name:'Расход', type:'line', data:c.closed||c.adSpend, smooth:0.4, symbol:'none', lineStyle:{width:2,color:'#3b82f6',type:'dashed'}, areaStyle:{color:new echarts.graphic.LinearGradient(0,0,0,1,[{offset:0,color:'rgba(59,130,246,0.08)'},{offset:1,color:'rgba(59,130,246,0.01)'}])} },
|
||
],
|
||
animationDuration: 600,
|
||
};
|
||
} else if (c.type === 'bar-compare') {
|
||
opt = {
|
||
tooltip: { trigger:'axis', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
|
||
legend: { data:['План','Факт'], textStyle:{color:'#52525b',fontSize:10}, top:0 },
|
||
grid: { left:44, right:16, top:28, bottom:28 },
|
||
xAxis: { type:'category', data:c.labels, axisLine:{lineStyle:{color:'#1e1e22'}}, axisLabel:{color:'#52525b',fontSize:10} },
|
||
yAxis: { type:'value', splitLine:{lineStyle:{color:'#1a1a1e'}}, axisLabel:{color:'#52525b',fontSize:10,formatter:'{value}К'} },
|
||
series: [
|
||
{ name:'План', type:'bar', data:c.plan, barWidth:'35%', itemStyle:{color:'rgba(59,130,246,0.15)',borderRadius:[4,4,0,0]} },
|
||
{ name:'Факт', type:'bar', data:c.fact, barWidth:'35%', itemStyle:{color:'#3b82f6',borderRadius:[4,4,0,0]} },
|
||
],
|
||
animationDuration: 600,
|
||
};
|
||
}
|
||
charts.mc.setOption(opt, true);
|
||
}
|
||
|
||
// ============================================
|
||
// SIDE CHART (bar horizontal)
|
||
// ============================================
|
||
function renderSide(seg) {
|
||
const c = seg.sideChart;
|
||
const el = document.getElementById('sc');
|
||
if (!charts.sc) charts.sc = echarts.init(el);
|
||
document.getElementById('sc-title').textContent = c.title;
|
||
|
||
const colors = ['#22c55e','#06b6d4','#a855f7','#3b82f6','#f59e0b','#ef4444'];
|
||
const opt = {
|
||
tooltip: { trigger:'axis', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
|
||
grid: { left:80, right:30, top:10, bottom:10 },
|
||
xAxis: { type:'value', splitLine:{lineStyle:{color:'#1a1a1e'}}, axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'} },
|
||
yAxis: { type:'category', data:c.data.map(d=>d.name).reverse(), axisLine:{lineStyle:{color:'#1e1e22'}}, axisLabel:{color:'#a1a1aa',fontSize:10} },
|
||
series: [{
|
||
type:'bar',
|
||
data: c.data.map(d=>d.value).reverse(),
|
||
barWidth:38,
|
||
itemStyle: {
|
||
borderRadius:[0,5,5,0],
|
||
color: function(params) {
|
||
const c = colors[params.dataIndex % colors.length];
|
||
return new echarts.graphic.LinearGradient(0,0,1,0,[
|
||
{offset:0, color: c+'88'},
|
||
{offset:1, color: c}
|
||
]);
|
||
}
|
||
}
|
||
}],
|
||
animationDuration: 600,
|
||
};
|
||
charts.sc.setOption(opt, true);
|
||
}
|
||
|
||
// ============================================
|
||
// ROMI CHART (bar group)
|
||
// ============================================
|
||
function renderROMI(seg) {
|
||
const el = document.getElementById('ch1');
|
||
if (!charts.ch1) charts.ch1 = echarts.init(el);
|
||
const c = seg.romiChart;
|
||
|
||
const opt = {
|
||
tooltip: { trigger:'axis', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
|
||
grid: { left:44, right:16, top:16, bottom:28 },
|
||
xAxis: { type:'category', data:c.labels, axisLine:{lineStyle:{color:'#1e1e22'}}, axisLabel:{color:'#52525b',fontSize:10} },
|
||
yAxis: { type:'value', splitLine:{lineStyle:{color:'#1a1a1e'}}, axisLabel:{color:'#52525b',fontSize:9,formatter:'{value}%'} },
|
||
series: [{
|
||
name:'ROMI %',
|
||
type:'bar',
|
||
data:c.romi,
|
||
barWidth:'55%',
|
||
itemStyle: {
|
||
borderRadius:[4,4,0,0],
|
||
color: function(params) {
|
||
return params.value >= 250 ? '#22c55e' : (params.value >= 200 ? '#06b6d4' : '#f59e0b');
|
||
}
|
||
}
|
||
}],
|
||
animationDuration: 600,
|
||
};
|
||
charts.ch1.setOption(opt, true);
|
||
}
|
||
|
||
// ============================================
|
||
// FUNNEL
|
||
// ============================================
|
||
function renderFunnel(seg) {
|
||
const el = document.getElementById('ch2');
|
||
if (!charts.ch2) charts.ch2 = echarts.init(el);
|
||
const colors = ['#22c55e','#06b6d4','#a855f7','#ef4444'];
|
||
|
||
const opt = {
|
||
tooltip: { trigger:'item', backgroundColor:'#111', borderColor:'#222', textStyle:{color:'#fafafa',fontSize:11} },
|
||
series: [{
|
||
type:'funnel',
|
||
left:'10%', bottom:5, width:'80%',
|
||
min:0, max: seg.funnel.data[0].value,
|
||
minSize:'0%', maxSize:'100%',
|
||
sort:'descending', gap:4,
|
||
label: { show:true, position:'inside', color:'#fff', fontSize:11, formatter:'{b}\n{c} ({d}%)' },
|
||
labelLine: { show:false },
|
||
itemStyle: { borderColor:'#111', borderWidth:2 },
|
||
data: seg.funnel.data.map((d,i)=>({
|
||
name:d.name, value:d.value,
|
||
itemStyle: { color: colors[i%colors.length] }
|
||
})),
|
||
}],
|
||
animationDuration: 600,
|
||
};
|
||
charts.ch2.setOption(opt, true);
|
||
}
|
||
|
||
// ============================================
|
||
// METRICS LIST
|
||
// ============================================
|
||
function renderMetrics(seg) {
|
||
const el = document.getElementById('mlist');
|
||
el.innerHTML = seg.metrics.map(m => {
|
||
return `<div class="metric-item">
|
||
<span class="metric-label">${m.label}</span>
|
||
<span class="metric-val">${m.value}</span>
|
||
<div class="metric-bar"><div class="metric-fill" data-pct="${m.pct}" style="background:${m.color}"></div></div>
|
||
</div>`;
|
||
}).join('');
|
||
|
||
requestAnimationFrame(() => {
|
||
setTimeout(() => {
|
||
el.querySelectorAll('.metric-fill').forEach(f => f.style.width = f.dataset.pct + '%');
|
||
}, 100);
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// MASTER SWITCH
|
||
// ============================================
|
||
function seg(key) {
|
||
current = key;
|
||
const s = D[key];
|
||
|
||
// Buttons
|
||
document.querySelectorAll('.sw-btn').forEach(b => {
|
||
b.classList.toggle('active', b.dataset.seg === key);
|
||
});
|
||
|
||
renderKpis(s);
|
||
renderMain(s);
|
||
renderSide(s);
|
||
renderROMI(s);
|
||
renderFunnel(s);
|
||
renderMetrics(s);
|
||
}
|
||
|
||
// ============================================
|
||
// INIT
|
||
// ============================================
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Segment switcher — event delegation
|
||
document.getElementById('switcher').addEventListener('click', (e) => {
|
||
const btn = e.target.closest('.sw-btn');
|
||
if (!btn) return;
|
||
seg(btn.dataset.seg);
|
||
});
|
||
|
||
// Initial render
|
||
seg('ecom');
|
||
|
||
// Clock
|
||
function tick() {
|
||
const n = new Date();
|
||
document.getElementById('clock').textContent =
|
||
String(n.getHours()).padStart(2,'0') + ':' +
|
||
String(n.getMinutes()).padStart(2,'0') + ':' +
|
||
String(n.getSeconds()).padStart(2,'0');
|
||
}
|
||
tick();
|
||
setInterval(tick, 1000);
|
||
|
||
// Resize
|
||
const ro = new ResizeObserver(() => {
|
||
Object.values(charts).forEach(c => c.resize());
|
||
});
|
||
ro.observe(document.getElementById('dash'));
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|