1081 lines
31 KiB
HTML
1081 lines
31 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>
|
||
/* ============================================
|
||
DESIGN TOKENS
|
||
============================================ */
|
||
:root {
|
||
--color-bg: #0a0a0a;
|
||
--color-surface: #111111;
|
||
--color-surface-hover: #1a1a1a;
|
||
--color-border: rgba(255,255,255,0.06);
|
||
--color-border-hover: rgba(255,255,255,0.12);
|
||
--color-text-primary: #f5f5f5;
|
||
--color-text-secondary: #a1a1aa;
|
||
--color-text-muted: #52525b;
|
||
--accent: #22c55e;
|
||
--accent-hover: #16a34a;
|
||
--accent-dim: rgba(34,197,94,0.12);
|
||
--accent-secondary: #3b82f6;
|
||
--accent-secondary-dim: rgba(59,130,246,0.12);
|
||
--warning: #f59e0b;
|
||
--warning-dim: rgba(245,158,11,0.12);
|
||
--error: #ef4444;
|
||
--error-dim: rgba(239,68,68,0.12);
|
||
--font-primary: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
|
||
--ease-out: cubic-bezier(0.4, 0, 0.2, 1);
|
||
--duration-normal: 300ms;
|
||
}
|
||
|
||
/* ============================================
|
||
RESET & BASE
|
||
============================================ */
|
||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
body {
|
||
font-family: var(--font-primary);
|
||
background: var(--color-bg);
|
||
color: var(--color-text-primary);
|
||
line-height: 1.6;
|
||
-webkit-font-smoothing: antialiased;
|
||
overflow-x: hidden;
|
||
min-height: 100vh;
|
||
}
|
||
|
||
/* Noise texture overlay */
|
||
body::before {
|
||
content: '';
|
||
position: fixed;
|
||
inset: 0;
|
||
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
|
||
pointer-events: none;
|
||
z-index: 0;
|
||
}
|
||
|
||
/* ============================================
|
||
HEADER BAR
|
||
============================================ */
|
||
.dash-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 12px 24px;
|
||
background: rgba(17,17,17,0.8);
|
||
backdrop-filter: blur(12px);
|
||
border-bottom: 1px solid var(--color-border);
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 100;
|
||
}
|
||
|
||
.dash-logo {
|
||
font-family: var(--font-mono);
|
||
font-weight: 700;
|
||
font-size: 16px;
|
||
letter-spacing: -0.5px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.dash-logo-dot {
|
||
width: 8px;
|
||
height: 8px;
|
||
background: var(--accent);
|
||
border-radius: 50%;
|
||
animation: pulse-dot 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot {
|
||
0%, 100% { opacity: 1; box-shadow: 0 0 0 0 rgba(34,197,94,0.4); }
|
||
50% { opacity: 0.8; box-shadow: 0 0 0 6px rgba(34,197,94,0); }
|
||
}
|
||
|
||
.dash-status {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 12px;
|
||
color: var(--color-text-secondary);
|
||
}
|
||
|
||
.dash-status-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
background: var(--accent);
|
||
border-radius: 50%;
|
||
animation: pulse-dot 2s ease-in-out infinite;
|
||
}
|
||
|
||
.dash-time {
|
||
font-family: var(--font-mono);
|
||
font-size: 11px;
|
||
color: var(--color-text-muted);
|
||
}
|
||
|
||
/* ============================================
|
||
SEGMENT SWITCHER
|
||
============================================ */
|
||
.segment-switcher {
|
||
display: flex;
|
||
gap: 4px;
|
||
padding: 6px;
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 12px;
|
||
margin: 24px auto;
|
||
max-width: 600px;
|
||
position: relative;
|
||
z-index: 1;
|
||
}
|
||
|
||
.segment-btn {
|
||
flex: 1;
|
||
padding: 10px 16px;
|
||
border: none;
|
||
background: transparent;
|
||
color: var(--color-text-secondary);
|
||
font-family: var(--font-primary);
|
||
font-size: 13px;
|
||
font-weight: 500;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
transition: all var(--duration-normal) var(--ease-out);
|
||
position: relative;
|
||
z-index: 1;
|
||
white-space: nowrap;
|
||
}
|
||
|
||
.segment-btn:hover {
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
.segment-btn.active {
|
||
background: var(--accent-dim);
|
||
color: var(--accent);
|
||
}
|
||
|
||
.segment-btn .seg-icon {
|
||
margin-right: 6px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
/* ============================================
|
||
KPI CARDS
|
||
============================================ */
|
||
.kpi-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(4, 1fr);
|
||
gap: 12px;
|
||
padding: 0 24px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.kpi-card {
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 12px;
|
||
padding: 16px;
|
||
transition: all var(--duration-normal) var(--ease-out);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.kpi-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0;
|
||
left: 0;
|
||
width: 3px;
|
||
height: 100%;
|
||
opacity: 0;
|
||
transition: opacity var(--duration-normal) var(--ease-out);
|
||
}
|
||
|
||
.kpi-card:hover {
|
||
border-color: var(--color-border-hover);
|
||
transform: translateY(-1px);
|
||
}
|
||
|
||
.kpi-card:hover::before {
|
||
opacity: 1;
|
||
}
|
||
|
||
.kpi-card.positive::before { background: var(--accent); }
|
||
.kpi-card.negative::before { background: var(--error); }
|
||
.kpi-card.neutral::before { background: var(--accent-secondary); }
|
||
|
||
.kpi-label {
|
||
font-size: 11px;
|
||
color: var(--color-text-muted);
|
||
text-transform: uppercase;
|
||
letter-spacing: 0.08em;
|
||
font-weight: 600;
|
||
margin-bottom: 8px;
|
||
}
|
||
|
||
.kpi-value {
|
||
font-family: var(--font-mono);
|
||
font-size: 24px;
|
||
font-weight: 700;
|
||
margin-bottom: 6px;
|
||
line-height: 1.1;
|
||
}
|
||
|
||
.kpi-trend {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 4px;
|
||
font-size: 12px;
|
||
font-weight: 600;
|
||
padding: 2px 8px;
|
||
border-radius: 6px;
|
||
font-family: var(--font-mono);
|
||
}
|
||
|
||
.kpi-trend.up {
|
||
color: var(--accent);
|
||
background: var(--accent-dim);
|
||
}
|
||
|
||
.kpi-trend.down {
|
||
color: var(--error);
|
||
background: var(--error-dim);
|
||
}
|
||
|
||
.kpi-trend.stable {
|
||
color: var(--accent-secondary);
|
||
background: var(--accent-secondary-dim);
|
||
}
|
||
|
||
/* ============================================
|
||
CHART SECTIONS
|
||
============================================ */
|
||
.charts-grid {
|
||
display: grid;
|
||
grid-template-columns: 2fr 1fr;
|
||
gap: 12px;
|
||
padding: 12px 24px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.chart-card {
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
transition: all var(--duration-normal) var(--ease-out);
|
||
}
|
||
|
||
.chart-card:hover {
|
||
border-color: var(--color-border-hover);
|
||
}
|
||
|
||
.chart-title {
|
||
font-size: 13px;
|
||
font-weight: 600;
|
||
margin-bottom: 4px;
|
||
color: var(--color-text-primary);
|
||
}
|
||
|
||
.chart-subtitle {
|
||
font-size: 11px;
|
||
color: var(--color-text-muted);
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.chart-container {
|
||
width: 100%;
|
||
height: 260px;
|
||
}
|
||
|
||
/* ============================================
|
||
BOTTOM ROW
|
||
============================================ */
|
||
.bottom-grid {
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr 1fr;
|
||
gap: 12px;
|
||
padding: 12px 24px;
|
||
max-width: 1200px;
|
||
margin: 0 auto;
|
||
}
|
||
|
||
.bottom-card {
|
||
background: var(--color-surface);
|
||
border: 1px solid var(--color-border);
|
||
border-radius: 12px;
|
||
padding: 20px;
|
||
transition: all var(--duration-normal) var(--ease-out);
|
||
}
|
||
|
||
.bottom-card:hover {
|
||
border-color: var(--color-border-hover);
|
||
}
|
||
|
||
.bottom-card .chart-container {
|
||
height: 200px;
|
||
}
|
||
|
||
/* ============================================
|
||
TOP CHANNELS TABLE
|
||
============================================ */
|
||
.top-channels {
|
||
margin-top: 12px;
|
||
}
|
||
|
||
.channel-row {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 8px 0;
|
||
border-bottom: 1px solid var(--color-border);
|
||
font-size: 12px;
|
||
}
|
||
|
||
.channel-row:last-child {
|
||
border-bottom: none;
|
||
}
|
||
|
||
.channel-name {
|
||
color: var(--color-text-secondary);
|
||
flex: 1;
|
||
}
|
||
|
||
.channel-value {
|
||
font-family: var(--font-mono);
|
||
font-weight: 600;
|
||
color: var(--color-text-primary);
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.channel-bar-wrap {
|
||
width: 80px;
|
||
height: 4px;
|
||
background: var(--color-border);
|
||
border-radius: 2px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.channel-bar {
|
||
height: 100%;
|
||
border-radius: 2px;
|
||
background: var(--accent);
|
||
transition: width 0.8s var(--ease-out);
|
||
}
|
||
|
||
/* ============================================
|
||
SECTION TITLE (outside mockup)
|
||
============================================ */
|
||
.dash-section-title {
|
||
text-align: center;
|
||
padding: 48px 24px 24px;
|
||
}
|
||
|
||
.dash-section-title h2 {
|
||
font-size: clamp(1.8rem, 4vw, 2.5rem);
|
||
font-weight: 800;
|
||
margin-bottom: 8px;
|
||
letter-spacing: -0.5px;
|
||
}
|
||
|
||
.dash-section-title p {
|
||
color: var(--color-text-secondary);
|
||
font-size: 15px;
|
||
}
|
||
|
||
/* ============================================
|
||
RESPONSIVE
|
||
============================================ */
|
||
@media (max-width: 768px) {
|
||
.kpi-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
.charts-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.bottom-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
.dash-header {
|
||
padding: 10px 16px;
|
||
}
|
||
.kpi-value {
|
||
font-size: 20px;
|
||
}
|
||
.segment-switcher {
|
||
margin: 16px;
|
||
}
|
||
.segment-btn {
|
||
padding: 8px 10px;
|
||
font-size: 12px;
|
||
}
|
||
.segment-btn .seg-icon {
|
||
display: none;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.kpi-grid {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- Section Title -->
|
||
<div class="dash-section-title">
|
||
<h2>Все ключевые цифры — в одном месте</h2>
|
||
<p>Выберите тип бизнеса, чтобы увидеть, как выглядит дашборд под ваш бизнес</p>
|
||
</div>
|
||
|
||
<!-- Segment Switcher -->
|
||
<div class="segment-switcher" id="segment-switcher">
|
||
<button class="segment-btn active" data-segment="ecommerce" onclick="switchSegment('ecommerce')">
|
||
<span class="seg-icon">🛒</span>Интернет-магазин
|
||
</button>
|
||
<button class="segment-btn" data-segment="services" onclick="switchSegment('services')">
|
||
<span class="seg-icon">💼</span>Услуги
|
||
</button>
|
||
<button class="segment-btn" data-segment="production" onclick="switchSegment('production')">
|
||
<span class="seg-icon">🏭</span>Производство
|
||
</button>
|
||
</div>
|
||
|
||
<!-- Dashboard Mockup -->
|
||
<div id="dashboard-mockup">
|
||
<!-- Header -->
|
||
<div class="dash-header">
|
||
<div class="dash-logo">
|
||
<span class="dash-logo-dot"></span>
|
||
ROI24
|
||
</div>
|
||
<div class="dash-status">
|
||
<span class="dash-status-dot"></span>
|
||
<span>Live</span>
|
||
<span style="margin: 0 8px; color: var(--color-border);">|</span>
|
||
<span class="dash-time" id="dash-time"></span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- KPI Cards -->
|
||
<div class="kpi-grid" id="kpi-grid">
|
||
<!-- Populated by JS -->
|
||
</div>
|
||
|
||
<!-- Charts -->
|
||
<div class="charts-grid">
|
||
<div class="chart-card">
|
||
<div class="chart-title" id="main-chart-title">Выручка</div>
|
||
<div class="chart-subtitle" id="main-chart-subtitle">12 месяцев с трендом</div>
|
||
<div class="chart-container" id="main-chart"></div>
|
||
</div>
|
||
<div class="chart-card">
|
||
<div class="chart-title" id="side-chart-title">Ключевые метрики</div>
|
||
<div class="chart-subtitle">Текущий период</div>
|
||
<div class="chart-container" id="side-chart"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Bottom Row -->
|
||
<div class="bottom-grid">
|
||
<div class="bottom-card">
|
||
<div class="chart-title">Каналы привлечения</div>
|
||
<div class="chart-subtitle">Доля в общем объёме</div>
|
||
<div class="chart-container" id="bottom-chart-1"></div>
|
||
</div>
|
||
<div class="bottom-card">
|
||
<div class="chart-title">Воронка</div>
|
||
<div class="chart-subtitle">Этапы конверсии</div>
|
||
<div class="chart-container" id="bottom-chart-2"></div>
|
||
</div>
|
||
<div class="bottom-card">
|
||
<div class="chart-title">Топ-5 метрик</div>
|
||
<div class="chart-subtitle">Выполнение плана</div>
|
||
<div class="top-channels" id="top-metrics"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ECharts CDN -->
|
||
<script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
|
||
|
||
<script>
|
||
// ============================================
|
||
// DATA FOR EACH SEGMENT
|
||
// ============================================
|
||
const segments = {
|
||
ecommerce: {
|
||
kpis: [
|
||
{ label: 'Выручка', value: '4.2М ₽', trend: '+12%', direction: 'up' },
|
||
{ label: 'Конверсия', value: '3.8%', trend: '+0.5%', direction: 'up' },
|
||
{ label: 'Ср. чек', value: '4 820 ₽', trend: '+3%', direction: 'up' },
|
||
{ label: 'Возвраты', value: '4.2%', trend: '-0.8%', direction: 'up' }, // negative is good for returns
|
||
],
|
||
mainChart: {
|
||
title: 'Выручка',
|
||
subtitle: '12 месяцев с трендом',
|
||
type: 'line',
|
||
data: [2.8, 3.1, 2.9, 3.4, 3.6, 3.2, 3.8, 4.0, 3.9, 4.2, 3.7, 4.2],
|
||
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
|
||
},
|
||
sideChart: {
|
||
title: 'Маржинальность по категориям',
|
||
type: 'bar-vertical',
|
||
data: [
|
||
{ name: 'Электроника', value: 18 },
|
||
{ name: 'Одежда', value: 35 },
|
||
{ name: 'Дом', value: 28 },
|
||
{ name: 'Спорт', value: 22 },
|
||
{ name: 'Красота', value: 42 },
|
||
{ name: 'Детский', value: 30 },
|
||
],
|
||
},
|
||
bottomCharts: {
|
||
channels: {
|
||
title: 'Каналы привлечения',
|
||
type: 'pie',
|
||
data: [
|
||
{ name: 'SEO', value: 35 },
|
||
{ name: 'Контекст', value: 28 },
|
||
{ name: 'Прямой', value: 22 },
|
||
{ name: 'Email', value: 8 },
|
||
{ name: 'Соцсети', value: 7 },
|
||
],
|
||
},
|
||
funnel: {
|
||
title: 'Воронка продаж',
|
||
data: [
|
||
{ name: 'Посетители', value: 45000 },
|
||
{ name: 'Корзина', value: 8100 },
|
||
{ name: 'Оформление', value: 4950 },
|
||
{ name: 'Оплата', value: 2723 },
|
||
],
|
||
},
|
||
topMetrics: [
|
||
{ name: 'Выручка (план)', value: 4.2, max: 4.5, pct: 93 },
|
||
{ name: 'Конверсия (план)', value: 3.8, max: 4.0, pct: 95 },
|
||
{ name: 'Ср. чек (план)', value: 4820, max: 5000, pct: 96 },
|
||
{ name: 'Повторные (план)', value: 32, max: 35, pct: 91 },
|
||
{ name: 'NPS (план)', value: 68, max: 75, pct: 91 },
|
||
],
|
||
},
|
||
},
|
||
services: {
|
||
kpis: [
|
||
{ label: 'Пайплайн', value: '12.8М ₽', trend: '+18%', direction: 'up' },
|
||
{ label: 'Конверсия', value: '24%', trend: '+3%', direction: 'up' },
|
||
{ label: 'Маржа проекта', value: '34%', trend: '+2%', direction: 'up' },
|
||
{ label: 'Загрузка', value: '87%', trend: '0%', direction: 'stable' },
|
||
],
|
||
mainChart: {
|
||
title: 'Пайплайн',
|
||
subtitle: '12 месяцев, млн ₽',
|
||
type: 'area',
|
||
data: [6.2, 7.0, 7.5, 8.1, 8.8, 9.2, 9.8, 10.5, 11.0, 11.5, 12.2, 12.8],
|
||
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
|
||
},
|
||
sideChart: {
|
||
title: 'Маржинальность по проектам',
|
||
type: 'bar-vertical',
|
||
data: [
|
||
{ name: 'Консалтинг', value: 42 },
|
||
{ name: 'Дизайн', value: 38 },
|
||
{ name: 'Разработка', value: 31 },
|
||
{ name: 'Маркетинг', value: 35 },
|
||
{ name: 'SEO', value: 45 },
|
||
{ name: 'Поддержка', value: 55 },
|
||
],
|
||
},
|
||
bottomCharts: {
|
||
channels: {
|
||
title: 'Источники клиентов',
|
||
type: 'pie',
|
||
data: [
|
||
{ name: 'Рефералы', value: 38 },
|
||
{ name: 'Сайт', value: 27 },
|
||
{ name: 'Telegram', value: 18 },
|
||
{ name: 'Конференции', value: 10 },
|
||
{ name: 'Холодные', value: 7 },
|
||
],
|
||
},
|
||
funnel: {
|
||
title: 'Воронка продаж',
|
||
data: [
|
||
{ name: 'Лиды', value: 320 },
|
||
{ name: 'Квалификация', value: 224 },
|
||
{ name: 'Предложение', value: 128 },
|
||
{ name: 'Сделка', value: 77 },
|
||
],
|
||
},
|
||
topMetrics: [
|
||
{ name: 'Пайплайн (план)', value: 12.8, max: 15.0, pct: 85 },
|
||
{ name: 'Конверсия (план)', value: 24, max: 25, pct: 96 },
|
||
{ name: 'Маржа (план)', value: 34, max: 35, pct: 97 },
|
||
{ name: 'Загрузка (план)', value: 87, max: 90, pct: 97 },
|
||
{ name: 'NPS (план)', value: 72, max: 75, pct: 96 },
|
||
],
|
||
},
|
||
},
|
||
production: {
|
||
kpis: [
|
||
{ label: 'Объём выпуска', value: '15 420 шт', trend: '+8%', direction: 'up' },
|
||
{ label: 'OEE', value: '78%', trend: '+4%', direction: 'up' },
|
||
{ label: 'Брак', value: '2.1%', trend: '-0.5%', direction: 'up' },
|
||
{ label: 'Запасы', value: '18.5М ₽', trend: '-5%', direction: 'down' },
|
||
],
|
||
mainChart: {
|
||
title: 'План/факт выпуска',
|
||
subtitle: '12 месяцев, тыс. шт',
|
||
type: 'bar-compare',
|
||
planData: [12, 13, 12.5, 14, 14.5, 13, 15, 15.5, 15, 15.5, 14, 16],
|
||
factData: [11.5, 12.8, 12.2, 13.8, 14.2, 12.5, 14.8, 15.2, 14.7, 15.0, 13.8, 15.4],
|
||
labels: ['Янв', 'Фев', 'Мар', 'Апр', 'Май', 'Июн', 'Июл', 'Авг', 'Сен', 'Окт', 'Ноя', 'Дек'],
|
||
},
|
||
sideChart: {
|
||
title: 'Себестоимость по цехам',
|
||
type: 'bar-horizontal',
|
||
data: [
|
||
{ name: 'Цех №1', value: 2.4 },
|
||
{ name: 'Цех №2', value: 3.1 },
|
||
{ name: 'Цех №3', value: 1.8 },
|
||
{ name: 'Цех №4', value: 2.7 },
|
||
{ name: 'Цех №5', value: 1.5 },
|
||
],
|
||
},
|
||
bottomCharts: {
|
||
channels: {
|
||
title: 'Структура затрат',
|
||
type: 'pie',
|
||
data: [
|
||
{ name: 'Сырьё', value: 45 },
|
||
{ name: 'Зарплата', value: 25 },
|
||
{ name: 'Энергия', value: 12 },
|
||
{ name: 'Логистика', value: 10 },
|
||
{ name: 'Прочее', value: 8 },
|
||
],
|
||
},
|
||
funnel: {
|
||
title: 'Качество (воронка)',
|
||
data: [
|
||
{ name: 'Выпуск', value: 15420 },
|
||
{ name: 'Контроль', value: 15200 },
|
||
{ name: 'Брак', value: 324 },
|
||
{ name: 'На доработку', value: 180 },
|
||
],
|
||
},
|
||
topMetrics: [
|
||
{ name: 'Выпуск (план)', value: 15.4, max: 16.0, pct: 96 },
|
||
{ name: 'OEE (план)', value: 78, max: 85, pct: 92 },
|
||
{ name: 'Брак (план)', value: 2.1, max: 2.0, pct: 105 },
|
||
{ name: 'Запасы (план)', value: 18.5, max: 20.0, pct: 93 },
|
||
{ name: 'OEE (план)', value: 78, max: 80, pct: 98 },
|
||
],
|
||
},
|
||
},
|
||
};
|
||
|
||
// ============================================
|
||
// STATE
|
||
// ============================================
|
||
let currentSegment = 'ecommerce';
|
||
let mainChartInstance = null;
|
||
let sideChartInstance = null;
|
||
let bottomChart1Instance = null;
|
||
let bottomChart2Instance = null;
|
||
|
||
// ============================================
|
||
// RENDER FUNCTIONS
|
||
// ============================================
|
||
function renderKPIs(segment) {
|
||
const grid = document.getElementById('kpi-grid');
|
||
const kpis = segment.kpis;
|
||
|
||
grid.innerHTML = kpis.map((kpi, i) => {
|
||
const trendClass = kpi.direction === 'up' ? 'up' : (kpi.direction === 'down' ? 'down' : 'stable');
|
||
const cardClass = kpi.direction === 'up' ? 'positive' : (kpi.direction === 'down' ? 'negative' : 'neutral');
|
||
const arrow = kpi.direction === 'up' ? '↑' : (kpi.direction === 'down' ? '↓' : '→');
|
||
|
||
return `
|
||
<div class="kpi-card ${cardClass}" style="opacity:0; transform: translateY(20px);">
|
||
<div class="kpi-label">${kpi.label}</div>
|
||
<div class="kpi-value">${kpi.value}</div>
|
||
<span class="kpi-trend ${trendClass}">${arrow} ${kpi.trend}</span>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
// Animate in
|
||
requestAnimationFrame(() => {
|
||
const cards = grid.querySelectorAll('.kpi-card');
|
||
cards.forEach((card, i) => {
|
||
setTimeout(() => {
|
||
card.style.transition = 'opacity 0.5s var(--ease-out), transform 0.5s var(--ease-out)';
|
||
card.style.opacity = '1';
|
||
card.style.transform = 'translateY(0)';
|
||
}, i * 80);
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderMainChart(segment) {
|
||
const cfg = segment.mainChart;
|
||
const dom = document.getElementById('main-chart');
|
||
if (!mainChartInstance) {
|
||
mainChartInstance = echarts.init(dom);
|
||
}
|
||
|
||
document.getElementById('main-chart-title').textContent = cfg.title;
|
||
document.getElementById('main-chart-subtitle').textContent = cfg.subtitle;
|
||
|
||
let option;
|
||
|
||
if (cfg.type === 'line' || cfg.type === 'area') {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(17,17,17,0.95)',
|
||
borderColor: 'rgba(255,255,255,0.1)',
|
||
textStyle: { color: '#f5f5f5', fontSize: 12 },
|
||
},
|
||
grid: { left: 40, right: 20, top: 10, bottom: 30 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: cfg.labels,
|
||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 10 },
|
||
axisTick: { show: false },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 10 },
|
||
},
|
||
series: [{
|
||
data: cfg.data,
|
||
type: cfg.type === 'area' ? 'line' : 'line',
|
||
smooth: 0.4,
|
||
symbol: 'none',
|
||
lineStyle: { width: 2.5, color: '#22c55e' },
|
||
areaStyle: cfg.type === 'area' ? {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||
{ offset: 0, color: 'rgba(34,197,94,0.25)' },
|
||
{ offset: 1, color: 'rgba(34,197,94,0.01)' },
|
||
])
|
||
} : undefined,
|
||
}],
|
||
animationDuration: 600,
|
||
animationEasing: 'cubicOut',
|
||
};
|
||
} else if (cfg.type === 'bar-compare') {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(17,17,17,0.95)',
|
||
borderColor: 'rgba(255,255,255,0.1)',
|
||
textStyle: { color: '#f5f5f5', fontSize: 12 },
|
||
},
|
||
legend: {
|
||
data: ['План', 'Факт'],
|
||
textStyle: { color: '#52525b', fontSize: 10 },
|
||
top: 0,
|
||
right: 0,
|
||
},
|
||
grid: { left: 40, right: 20, top: 30, bottom: 30 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: cfg.labels,
|
||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 10 },
|
||
axisTick: { show: false },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 10 },
|
||
},
|
||
series: [
|
||
{
|
||
name: 'План',
|
||
data: cfg.planData,
|
||
type: 'bar',
|
||
barWidth: '30%',
|
||
itemStyle: { color: 'rgba(59,130,246,0.2)', borderRadius: [4, 4, 0, 0] },
|
||
},
|
||
{
|
||
name: 'Факт',
|
||
data: cfg.factData,
|
||
type: 'bar',
|
||
barWidth: '30%',
|
||
itemStyle: { color: '#3b82f6', borderRadius: [4, 4, 0, 0] },
|
||
},
|
||
],
|
||
animationDuration: 600,
|
||
animationEasing: 'cubicOut',
|
||
};
|
||
}
|
||
|
||
mainChartInstance.setOption(option, true);
|
||
}
|
||
|
||
function renderSideChart(segment) {
|
||
const cfg = segment.sideChart;
|
||
const dom = document.getElementById('side-chart');
|
||
if (!sideChartInstance) {
|
||
sideChartInstance = echarts.init(dom);
|
||
}
|
||
|
||
document.getElementById('side-chart-title').textContent = cfg.title;
|
||
|
||
const data = cfg.data.map(d => d.value);
|
||
const names = cfg.data.map(d => d.name);
|
||
|
||
const colors = ['#22c55e', '#3b82f6', '#22c55e', '#3b82f6', '#22c55e', '#3b82f6'];
|
||
|
||
let option;
|
||
|
||
if (cfg.type === 'bar-vertical') {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(17,17,17,0.95)',
|
||
borderColor: 'rgba(255,255,255,0.1)',
|
||
textStyle: { color: '#f5f5f5', fontSize: 12 },
|
||
},
|
||
grid: { left: 10, right: 10, top: 10, bottom: 40 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: names,
|
||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 9, rotate: 0 },
|
||
axisTick: { show: false },
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 9, formatter: '{value}%' },
|
||
},
|
||
series: [{
|
||
data: data.map((v, i) => ({
|
||
value: v,
|
||
itemStyle: { color: v >= 30 ? '#22c55e' : '#f59e0b', borderRadius: [3, 3, 0, 0] },
|
||
})),
|
||
type: 'bar',
|
||
barWidth: '45%',
|
||
}],
|
||
animationDuration: 600,
|
||
animationEasing: 'cubicOut',
|
||
};
|
||
} else if (cfg.type === 'bar-horizontal') {
|
||
option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
backgroundColor: 'rgba(17,17,17,0.95)',
|
||
borderColor: 'rgba(255,255,255,0.1)',
|
||
textStyle: { color: '#f5f5f5', fontSize: 12 },
|
||
},
|
||
grid: { left: 60, right: 30, top: 10, bottom: 10 },
|
||
xAxis: {
|
||
type: 'value',
|
||
splitLine: { lineStyle: { color: 'rgba(255,255,255,0.04)' } },
|
||
axisLabel: { color: '#52525b', fontSize: 9, formatter: '{value}М' },
|
||
},
|
||
yAxis: {
|
||
type: 'category',
|
||
data: names.reverse(),
|
||
axisLine: { lineStyle: { color: 'rgba(255,255,255,0.08)' } },
|
||
axisLabel: { color: '#a1a1aa', fontSize: 10 },
|
||
axisTick: { show: false },
|
||
},
|
||
series: [{
|
||
data: cfg.data.map(d => d.value).reverse(),
|
||
type: 'bar',
|
||
barWidth: '40%',
|
||
itemStyle: {
|
||
color: new echarts.graphic.LinearGradient(0, 0, 1, 0, [
|
||
{ offset: 0, color: '#3b82f6' },
|
||
{ offset: 1, color: '#22c55e' },
|
||
]),
|
||
borderRadius: [0, 4, 4, 0],
|
||
},
|
||
}],
|
||
animationDuration: 600,
|
||
animationEasing: 'cubicOut',
|
||
};
|
||
}
|
||
|
||
sideChartInstance.setOption(option, true);
|
||
}
|
||
|
||
function renderBottomCharts(segment) {
|
||
const cfg = segment.bottomCharts;
|
||
|
||
// Channel pie chart
|
||
const pieDom = document.getElementById('bottom-chart-1');
|
||
if (!bottomChart1Instance) {
|
||
bottomChart1Instance = echarts.init(pieDom);
|
||
}
|
||
const pieData = cfg.channels.data.map(d => ({
|
||
name: d.name,
|
||
value: d.value,
|
||
}));
|
||
const pieColors = ['#22c55e', '#3b82f6', '#f59e0b', '#a855f7', '#ef4444'];
|
||
|
||
bottomChart1Instance.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(17,17,17,0.95)',
|
||
borderColor: 'rgba(255,255,255,0.1)',
|
||
textStyle: { color: '#f5f5f5', fontSize: 12 },
|
||
},
|
||
series: [{
|
||
type: 'pie',
|
||
radius: ['45%', '70%'],
|
||
center: ['50%', '55%'],
|
||
avoidLabelOverlap: false,
|
||
itemStyle: { borderRadius: 6, borderColor: '#111111', borderWidth: 3 },
|
||
label: { show: false },
|
||
labelLine: { show: false },
|
||
data: pieData.map((d, i) => ({ ...d, itemStyle: { color: pieColors[i % pieColors.length] } })),
|
||
emphasis: {
|
||
itemStyle: { shadowBlur: 10, shadowOffsetX: 0, shadowColor: 'rgba(0,0,0,0.3)' },
|
||
},
|
||
}],
|
||
animationDuration: 600,
|
||
}, true);
|
||
|
||
// Funnel chart
|
||
const funnelDom = document.getElementById('bottom-chart-2');
|
||
if (!bottomChart2Instance) {
|
||
bottomChart2Instance = echarts.init(funnelDom);
|
||
}
|
||
const funnelData = cfg.funnel.data.map(d => ({ name: d.name, value: d.value }));
|
||
const funnelColors = ['#22c55e', '#3b82f6', '#f59e0b', '#ef4444'];
|
||
|
||
bottomChart2Instance.setOption({
|
||
tooltip: {
|
||
trigger: 'item',
|
||
backgroundColor: 'rgba(17,17,17,0.95)',
|
||
borderColor: 'rgba(255,255,255,0.1)',
|
||
textStyle: { color: '#f5f5f5', fontSize: 12 },
|
||
},
|
||
series: [{
|
||
type: 'funnel',
|
||
left: '10%',
|
||
top: 5,
|
||
bottom: 5,
|
||
width: '80%',
|
||
min: 0,
|
||
max: funnelData[0].value,
|
||
minSize: '0%',
|
||
maxSize: '100%',
|
||
sort: 'descending',
|
||
gap: 4,
|
||
label: {
|
||
show: true,
|
||
position: 'inside',
|
||
color: '#fff',
|
||
fontSize: 11,
|
||
formatter: '{b}\n{c}',
|
||
},
|
||
labelLine: { show: false },
|
||
itemStyle: { borderColor: '#111', borderWidth: 2 },
|
||
data: funnelData.map((d, i) => ({
|
||
...d,
|
||
itemStyle: { color: funnelColors[i % funnelColors.length] },
|
||
})),
|
||
}],
|
||
animationDuration: 600,
|
||
}, true);
|
||
|
||
// Top metrics bars
|
||
const metricsDom = document.getElementById('top-metrics');
|
||
metricsDom.innerHTML = cfg.topMetrics.map(m => {
|
||
const pctColor = m.pct >= 95 ? 'var(--accent)' : (m.pct >= 85 ? 'var(--accent-secondary)' : 'var(--warning)');
|
||
const valDisplay = m.name.includes('Брак') ? m.value + '%' :
|
||
(typeof m.value === 'number' && m.value > 100 ? m.value.toLocaleString('ru-RU') : m.value);
|
||
return `
|
||
<div class="channel-row">
|
||
<span class="channel-name">${m.name}</span>
|
||
<span class="channel-value">${valDisplay}</span>
|
||
<div class="channel-bar-wrap">
|
||
<div class="channel-bar" style="width:0%; background:${pctColor};" data-target="${m.pct}"></div>
|
||
</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
|
||
// Animate bars
|
||
requestAnimationFrame(() => {
|
||
setTimeout(() => {
|
||
document.querySelectorAll('.channel-bar').forEach(bar => {
|
||
bar.style.width = bar.dataset.target + '%';
|
||
});
|
||
}, 200);
|
||
});
|
||
}
|
||
|
||
function updateDashTime() {
|
||
const now = new Date();
|
||
const h = String(now.getHours()).padStart(2, '0');
|
||
const m = String(now.getMinutes()).padStart(2, '0');
|
||
const s = String(now.getSeconds()).padStart(2, '0');
|
||
document.getElementById('dash-time').textContent = `${h}:${m}:${s}`;
|
||
}
|
||
|
||
// ============================================
|
||
// MAIN SWITCH FUNCTION
|
||
// ============================================
|
||
function switchSegment(segmentKey) {
|
||
const segment = segments[segmentKey];
|
||
if (!segment) return;
|
||
|
||
currentSegment = segmentKey;
|
||
renderKPIs(segment);
|
||
renderMainChart(segment);
|
||
renderSideChart(segment);
|
||
renderBottomCharts(segment);
|
||
|
||
// Update switcher buttons
|
||
document.querySelectorAll('.segment-btn').forEach(btn => {
|
||
btn.classList.toggle('active', btn.dataset.segment === segmentKey);
|
||
});
|
||
}
|
||
|
||
// ============================================
|
||
// INIT
|
||
// ============================================
|
||
document.addEventListener('DOMContentLoaded', () => {
|
||
// Expose to global scope for onclick handlers
|
||
window.switchSegment = switchSegment;
|
||
|
||
// Initial render
|
||
switchSegment('ecommerce');
|
||
|
||
// Update time
|
||
updateDashTime();
|
||
setInterval(updateDashTime, 1000);
|
||
|
||
// Resize handler
|
||
window.addEventListener('resize', () => {
|
||
mainChartInstance?.resize();
|
||
sideChartInstance?.resize();
|
||
bottomChart1Instance?.resize();
|
||
bottomChart2Instance?.resize();
|
||
});
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|