feat: Bloomberg-style sparklines in KPI cards + increased section spacing
This commit is contained in:
1 parent
bd033f5d36
commit
6d59cf206f
1 file changed
+79
-40
+79
-40
@@ -67,7 +67,7 @@ body {
|
||||
/* PAGE HEADER */
|
||||
.page-header {
|
||||
max-width: 1280px; margin: 0 auto;
|
||||
padding: 32px 32px 0;
|
||||
padding: 40px 32px 0;
|
||||
}
|
||||
.page-title {
|
||||
font-size: 28px; font-weight: 700;
|
||||
@@ -79,12 +79,13 @@ body {
|
||||
/* TABS */
|
||||
.tabs-section {
|
||||
max-width: 1280px; margin: 0 auto;
|
||||
padding: 24px 32px 0;
|
||||
padding: 32px 32px 0;
|
||||
}
|
||||
.tabs {
|
||||
display: flex; align-items: center; gap: 0;
|
||||
border-bottom: 1px solid var(--border-strong);
|
||||
overflow-x: auto; scrollbar-width: none;
|
||||
margin-bottom: 28px;
|
||||
}
|
||||
.tabs::-webkit-scrollbar { display: none; }
|
||||
.tab-btn {
|
||||
@@ -108,50 +109,59 @@ body {
|
||||
/* KPI GRID — Bloomberg ticker style */
|
||||
.kpi-grid {
|
||||
display: grid; grid-template-columns: repeat(6, 1fr);
|
||||
gap: 10px; padding: 0;
|
||||
gap: 14px; padding: 0;
|
||||
max-width: 1280px; margin: 0 auto;
|
||||
}
|
||||
.kpi {
|
||||
background: #121212;
|
||||
border: 1px solid #2a2a2a;
|
||||
border-radius: 6px;
|
||||
padding: 14px 16px;
|
||||
background: #0e0e0e;
|
||||
border: 1px solid #1e1e1e;
|
||||
border-radius: 8px;
|
||||
padding: 16px 18px 14px;
|
||||
position: relative;
|
||||
transition: all 0.2s var(--ease);
|
||||
display: flex; flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: all 0.25s var(--ease);
|
||||
opacity: 0; transform: translateY(8px);
|
||||
}
|
||||
.kpi.visible { opacity: 1; transform: translateY(0); }
|
||||
.kpi:hover {
|
||||
border-color: #3a3a3a;
|
||||
background: #181818;
|
||||
transform: translateY(-1px);
|
||||
border-color: #2e2e2e;
|
||||
background: #131313;
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 4px 20px rgba(0,0,0,0.3);
|
||||
}
|
||||
.kpi-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 10px; }
|
||||
.kpi-top { display: flex; align-items: center; justify-content: space-between; }
|
||||
.kpi-name {
|
||||
font-size: 10px; font-weight: 600;
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
color: #71717a;
|
||||
text-transform: uppercase; letter-spacing: 0.1em;
|
||||
color: #666;
|
||||
}
|
||||
.kpi-badge {
|
||||
font-family: var(--mono); font-size: 10px; font-weight: 600;
|
||||
padding: 2px 6px; border-radius: 3px;
|
||||
padding: 2px 7px; border-radius: 4px;
|
||||
display: inline-flex; align-items: center; gap: 2px;
|
||||
letter-spacing: 0;
|
||||
}
|
||||
.kpi-badge.up { color: #22c55e; background: rgba(34,197,94,0.1); }
|
||||
.kpi-badge.down { color: #ef4444; background: rgba(239,68,68,0.1); }
|
||||
.kpi-badge.neutral { color: #3b82f6; background: rgba(59,130,246,0.1); }
|
||||
.kpi-badge.up { color: #22c55e; background: rgba(34,197,94,0.08); }
|
||||
.kpi-badge.down { color: #ef4444; background: rgba(239,68,68,0.08); }
|
||||
.kpi-badge.neutral { color: #3b82f6; background: rgba(59,130,246,0.08); }
|
||||
.kpi-value {
|
||||
font-family: var(--mono);
|
||||
font-size: 22px; font-weight: 700;
|
||||
font-size: 24px; font-weight: 700;
|
||||
letter-spacing: -0.5px; line-height: 1.1;
|
||||
color: #f4f4f4; margin-bottom: 4px;
|
||||
color: #f4f4f4;
|
||||
}
|
||||
.kpi-context { font-size: 10px; color: #52525b; }
|
||||
.sparkline-wrap {
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.kpi-context { font-size: 10px; color: #4a4a4a; line-height: 1.4; }
|
||||
|
||||
/* CHARTS WRAPPER */
|
||||
.charts-wrap {
|
||||
max-width: 1280px; margin: 0 auto;
|
||||
padding: 0 32px 32px;
|
||||
padding: 28px 32px 32px;
|
||||
}
|
||||
.charts-grid {
|
||||
display: grid; gap: 1px;
|
||||
@@ -240,12 +250,12 @@ body {
|
||||
const D = {
|
||||
ecom: {
|
||||
kpis: [
|
||||
{ n:'Выручка', v:'4.2М ₽', d:'+12%', t:'up', p:'vs прошл. мес' },
|
||||
{ n:'AOV', v:'4 820 ₽', d:'+3%', t:'up', p:'ср. чек' },
|
||||
{ n:'CVR', v:'3.8%', d:'+0.5%', t:'up', p:'клики→покупка' },
|
||||
{ n:'CAC', v:'680 ₽', d:'−8%', t:'down', p:'стоим. клиента' },
|
||||
{ n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес' },
|
||||
{ n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы' },
|
||||
{ n:'Выручка', v:'4.2М ₽', d:'+12%', t:'up', p:'vs прошл. мес', spark:[2.8,3.1,2.9,3.4,3.6,3.2,3.8,4.0,3.9,4.2,3.7,4.2] },
|
||||
{ n:'AOV', v:'4 820 ₽', d:'+3%', t:'up', p:'ср. чек', spark:[4200,4500,4350,4600,4700,4550,4800,4750,4650,4820,4700,4820] },
|
||||
{ n:'CVR', v:'3.8%', d:'+0.5%', t:'up', p:'клики→покупка', spark:[3.2,3.3,3.1,3.4,3.5,3.3,3.6,3.7,3.5,3.8,3.6,3.8] },
|
||||
{ n:'CAC', v:'680 ₽', d:'−8%', t:'down', p:'стоим. клиента', spark:[800,750,780,720,700,730,710,690,670,680,690,680] },
|
||||
{ n:'CLV', v:'12 400 ₽', d:'+18%', t:'up', p:'за 6 мес', spark:[10000,10500,10200,11000,11200,10800,11500,11800,12000,12200,12100,12400] },
|
||||
{ n:'ROAS', v:'4.2×', d:'+15%', t:'up', p:'возврат рекламы', spark:[3.5,3.6,3.7,3.8,3.9,3.7,4.0,4.1,4.0,4.2,4.1,4.2] },
|
||||
],
|
||||
mc: { title:'Выручка и расходы на рекламу', sub:'12 месяцев · выручка (зелёный) vs маркетинг-бюджет (пунктир)', type:'dual',
|
||||
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||||
@@ -278,12 +288,12 @@ const D = {
|
||||
},
|
||||
services: {
|
||||
kpis: [
|
||||
{ n:'Пайплайн', v:'12.8М ₽', d:'+18%', t:'up', p:'открытых сделок' },
|
||||
{ n:'Win Rate', v:'24%', d:'+3%', t:'up', p:'закрыто/всего' },
|
||||
{ n:'Sales Cycle', v:'32 дн', d:'−5%', t:'down', p:'длительность' },
|
||||
{ n:'Pipeline Cov.', v:'4.1×', d:'+0.6', t:'up', p:'покрытие квоты' },
|
||||
{ n:'CPL', v:'2 400 ₽', d:'−12%', t:'down', p:'стоимость лида' },
|
||||
{ n:'ROMI', v:'245%', d:'+35%', t:'up', p:'маркетинг ROI' },
|
||||
{ n:'Пайплайн', v:'12.8М ₽', d:'+18%', t:'up', p:'открытых сделок', spark:[6.2,7.0,7.5,8.1,8.8,9.2,9.8,10.5,11.0,11.5,12.2,12.8] },
|
||||
{ n:'Win Rate', v:'24%', d:'+3%', t:'up', p:'закрыто/всего', spark:[20,21,22,21,22,23,22,23,24,23,24,24] },
|
||||
{ n:'Sales Cycle', v:'32 дн', d:'−5%', t:'down', p:'длительность', spark:[35,34,34,33,34,33,33,32,33,32,33,32] },
|
||||
{ n:'Pipeline Cov.', v:'4.1×', d:'+0.6', t:'up', p:'покрытие квоты', spark:[3.2,3.4,3.5,3.6,3.7,3.8,3.9,3.9,4.0,4.0,4.1,4.1] },
|
||||
{ n:'CPL', v:'2 400 ₽', d:'−12%', t:'down', p:'стоимость лида', spark:[3200,3000,3100,2900,2800,2900,2700,2600,2500,2400,2500,2400] },
|
||||
{ n:'ROMI', v:'245%', d:'+35%', t:'up', p:'маркетинг ROI', spark:[200,210,220,225,230,228,235,240,238,245,242,245] },
|
||||
],
|
||||
mc: { title:'Пайплайн и закрытые сделки', sub:'12 месяцев · пайплайн (синий) vs закрытие (зелёный)', type:'dual',
|
||||
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||||
@@ -316,12 +326,12 @@ const D = {
|
||||
},
|
||||
production: {
|
||||
kpis: [
|
||||
{ n:'Выпуск', v:'15 420 шт', d:'+8%', t:'up', p:'к плану' },
|
||||
{ n:'OEE', v:'78%', d:'+4%', t:'up', p:'загрузка мощностей' },
|
||||
{ n:'Брак', v:'2.1%', d:'−0.5%', t:'down', p:'дефекты' },
|
||||
{ n:'Себест.', v:'3 200 ₽', d:'−3%', t:'down', p:'за единицу' },
|
||||
{ n:'Оборачив.', v:'5.2×', d:'+0.3', t:'up', p:'запасов в год' },
|
||||
{ n:'ROMI рекламы', v:'310%', d:'+45%', t:'up', p:'маркетинг ROI' },
|
||||
{ n:'Выпуск', v:'15 420 шт', d:'+8%', t:'up', p:'к плану', spark:[11500,12800,12200,13800,14200,12500,14800,15200,14700,15000,13800,15420] },
|
||||
{ n:'OEE', v:'78%', d:'+4%', t:'up', p:'загрузка мощностей', spark:[72,73,74,73,74,75,76,76,77,77,78,78] },
|
||||
{ n:'Брак', v:'2.1%', d:'−0.5%', t:'down', p:'дефекты', spark:[2.8,2.7,2.6,2.5,2.5,2.6,2.4,2.3,2.3,2.2,2.2,2.1] },
|
||||
{ n:'Себест.', v:'3 200 ₽', d:'−3%', t:'down', p:'за единицу', spark:[3500,3450,3400,3400,3350,3380,3320,3280,3250,3220,3250,3200] },
|
||||
{ n:'Оборачив.', v:'5.2×', d:'+0.3', t:'up', p:'запасов в год', spark:[4.5,4.6,4.7,4.8,4.9,4.9,5.0,5.1,5.1,5.2,5.1,5.2] },
|
||||
{ n:'ROMI рекламы', v:'310%', d:'+45%', t:'up', p:'маркетинг ROI', spark:[250,260,265,270,275,280,285,290,295,300,305,310] },
|
||||
],
|
||||
mc: { title:'План / факт выпуска', sub:'12 месяцев · план (пунктир) vs фактический выпуск (сплошная)', type:'bar-cmp',
|
||||
labels:['Янв','Фев','Мар','Апр','Май','Июн','Июл','Авг','Сен','Окт','Ноя','Дек'],
|
||||
@@ -372,11 +382,40 @@ function seg(key) {
|
||||
<span class="kpi-badge ${d.t}">${arrow} ${d.d}</span>
|
||||
</div>
|
||||
<div class="kpi-value">${d.v}</div>
|
||||
<div class="sparkline-wrap" id="spark-${i}"></div>
|
||||
<div class="kpi-context">${d.p}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
requestAnimationFrame(() => g.querySelectorAll('.kpi').forEach(c => c.classList.add('visible')));
|
||||
|
||||
// Sparkline charts
|
||||
const sparkColors = [];
|
||||
s.kpis.forEach(d => { sparkColors.push(d.t === 'up' ? '#22c55e' : d.t === 'down' ? '#ef4444' : '#3b82f6'); });
|
||||
s.kpis.forEach((d, i) => {
|
||||
if (d.spark) {
|
||||
const sparkEl = document.getElementById(`spark-${i}`);
|
||||
if (!sparkEl) return;
|
||||
const sparkChart = echarts.init(sparkEl, null, { renderer: 'canvas' });
|
||||
const isDownTrend = d.t === 'down' && d.p.includes('стоим') || d.p.includes('дефект') || d.p.includes('за единиц') || d.p.includes('стоим');
|
||||
sparkChart.setOption({
|
||||
grid: { left: 0, right: 0, top: 4, bottom: 4 },
|
||||
xAxis: { type: 'category', show: false, data: d.spark },
|
||||
yAxis: { type: 'value', show: false },
|
||||
series: [{
|
||||
type: 'line', data: d.spark, smooth: 0.4, symbol: 'none',
|
||||
lineStyle: { width: 2, color: sparkColors[i] },
|
||||
areaStyle: {
|
||||
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
|
||||
{ offset: 0, color: sparkColors[i] + '30' },
|
||||
{ offset: 1, color: sparkColors[i] + '05' }
|
||||
])
|
||||
},
|
||||
}],
|
||||
animationDuration: 800,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Main chart
|
||||
document.getElementById('mc-title').textContent = s.mc.title;
|
||||
document.getElementById('mc-sub').textContent = s.mc.sub;
|
||||
|
||||
Reference in new issue
Block a user