330 lines
12 KiB
HTML
330 lines
12 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru" data-theme="light">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>Калькулятор стоимости подключения</title>
|
||
<link rel="stylesheet" href="design-system/kt-ai-fonts.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-tokens.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-components.css">
|
||
<link rel="stylesheet" href="design-system/kt-ai-page.css">
|
||
<style>
|
||
.calc-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)); gap: var(--kt-ai-space-6); margin-top: var(--kt-ai-space-8); }
|
||
.calc-card { cursor: pointer; transition: all 0.2s ease; }
|
||
.calc-card:hover { transform: translateY(-2px); box-shadow: var(--kt-ai-shadow-lg); }
|
||
.calc-card.selected { border-color: var(--kt-ai-primary); background: var(--kt-ai-primary-muted); }
|
||
.price-badge { font-size: var(--kt-ai-text-xl); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-primary); }
|
||
.total-block { background: var(--kt-ai-card-bg); border: 1px solid var(--kt-ai-border); border-radius: var(--kt-ai-radius-3xl); padding: var(--kt-ai-space-8); text-align: center; margin-top: var(--kt-ai-space-10); }
|
||
.total-amount { font-size: clamp(32px, 5vw, 48px); font-weight: var(--kt-ai-weight-bold); color: var(--kt-ai-primary); margin: var(--kt-ai-space-4) 0; }
|
||
.radio-hidden { position: absolute; opacity: 0; pointer-events: none; }
|
||
.journal { margin-top: var(--kt-ai-space-8); padding: var(--kt-ai-space-6); background: var(--kt-ai-bg-soft); border-radius: var(--kt-ai-radius-xl); }
|
||
.journal-entry { padding: var(--kt-ai-space-3) 0; border-bottom: 1px solid var(--kt-ai-divider); font-size: var(--kt-ai-text-sm); }
|
||
.journal-entry:last-child { border-bottom: none; }
|
||
.reveal { opacity: 0; transform: translateY(20px); transition: opacity 0.6s ease, transform 0.6s ease; }
|
||
.reveal.in { opacity: 1; transform: none; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<header class="kt-ai-pagehead">
|
||
<div class="kt-ai-wrap kt-ai-pagehead-in">
|
||
<strong>Калькулятор подключения</strong>
|
||
<span class="kt-ai-spacer"></span>
|
||
<a class="kt-ai-btn" data-variant="ghost" href="#journal">Журнал</a>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="kt-ai-hero" data-align="center">
|
||
<div class="kt-ai-wrap">
|
||
<h1 class="reveal">Рассчитайте стоимость подключения</h1>
|
||
<p class="kt-ai-hero-sub reveal">Выберите тариф, скорость и оборудование — сумма обновится автоматически</p>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="kt-ai-section" data-divider="true">
|
||
<div class="kt-ai-wrap" data-width="wide">
|
||
|
||
<div class="calc-grid">
|
||
<div class="reveal">
|
||
<h2 style="margin-bottom: var(--kt-ai-space-4)">Тарифный план</h2>
|
||
<div id="tariffs" class="kt-ai-v" data-gap="tight">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reveal">
|
||
<h2 style="margin-bottom: var(--kt-ai-space-4)">Скорость соединения</h2>
|
||
<div id="speeds" class="kt-ai-v" data-gap="tight">
|
||
</div>
|
||
</div>
|
||
|
||
<div class="reveal">
|
||
<h2 style="margin-bottom: var(--kt-ai-space-4)">Оборудование</h2>
|
||
<div id="equipment" class="kt-ai-v" data-gap="tight">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="total-block reveal">
|
||
<span style="font-size: var(--kt-ai-text-lg); color: var(--kt-ai-fg-muted)">Итого в месяц</span>
|
||
<div class="total-amount" id="totalAmount">0 ₸</div>
|
||
<label class="kt-ai-card" style="display: inline-flex; align-items: center; gap: var(--kt-ai-space-3); padding: var(--kt-ai-space-4) var(--kt-ai-space-5); margin: var(--kt-ai-space-5) 0; cursor: pointer">
|
||
<input type="checkbox" id="discountCheck" style="width: 18px; height: 18px">
|
||
<span style="font-weight: var(--kt-ai-weight-medium)">Скидка 10% для сотрудников КТ</span>
|
||
</label>
|
||
<button class="kt-ai-btn" data-variant="primary" data-size="lg" id="copyBtn">
|
||
<svg class="kt-icon" style="margin-right: 8px"><use href="#copy"></use></svg>
|
||
Скопировать расчёт
|
||
</button>
|
||
<div id="copyStatus" style="margin-top: var(--kt-ai-space-3); font-size: var(--kt-ai-text-sm); color: var(--kt-ai-fg-muted); min-height: 20px;"></div>
|
||
</div>
|
||
|
||
<div id="journal" class="journal reveal">
|
||
<h3 style="margin-bottom: var(--kt-ai-space-4)">Журнал расчётов</h3>
|
||
<div id="journalList">
|
||
<div class="journal-entry" style="color: var(--kt-ai-fg-muted)">Пока нет записей</div>
|
||
</div>
|
||
</div>
|
||
|
||
</div>
|
||
</section>
|
||
|
||
<footer class="kt-ai-wrap kt-ai-pagefoot">
|
||
<span>© 2026 Калькулятор подключения</span>
|
||
<span class="kt-ai-spacer"></span>
|
||
<span style="color: var(--kt-ai-fg-muted)">Для менеджеров и клиентов</span>
|
||
</footer>
|
||
|
||
<script>
|
||
// Демо-данные из brief.md
|
||
const TARIFFS = [
|
||
{ id: 'basic', name: 'Базовый', price: 5000 },
|
||
{ id: 'optimal', name: 'Оптимальный', price: 7500 },
|
||
{ id: 'premium', name: 'Премиум', price: 12000 }
|
||
];
|
||
|
||
const SPEEDS = [
|
||
{ id: '100', name: '100 Мбит/с', price: 0 },
|
||
{ id: '500', name: '500 Мбит/с', price: 1000 },
|
||
{ id: '1000', name: '1 Гбит/с', price: 2000 }
|
||
];
|
||
|
||
const EQUIPMENT = [
|
||
{ id: 'own', name: 'Свой роутер', price: 0 },
|
||
{ id: 'wifi5', name: 'Аренда Wi-Fi 5', price: 1500 },
|
||
{ id: 'wifi6', name: 'Аренда Wi-Fi 6', price: 2500 }
|
||
];
|
||
|
||
// Состояние
|
||
let state = {
|
||
tariff: null,
|
||
speed: null,
|
||
equipment: null,
|
||
discount: false
|
||
};
|
||
|
||
// Инициализация
|
||
function init() {
|
||
loadState();
|
||
renderOptions();
|
||
updateTotal();
|
||
renderJournal();
|
||
setupReveal();
|
||
setupDiscount();
|
||
}
|
||
|
||
// Рендер опций
|
||
function renderOptions() {
|
||
const tariffsEl = document.getElementById('tariffs');
|
||
const speedsEl = document.getElementById('speeds');
|
||
const equipmentEl = document.getElementById('equipment');
|
||
|
||
tariffsEl.innerHTML = TARIFFS.map(t => renderCard('tariff', t.id, t.name, t.price, state.tariff === t.id)).join('');
|
||
speedsEl.innerHTML = SPEEDS.map(s => renderCard('speed', s.id, s.name, s.price, state.speed === s.id)).join('');
|
||
equipmentEl.innerHTML = EQUIPMENT.map(e => renderCard('equipment', e.id, e.name, e.price, state.equipment === e.id)).join('');
|
||
|
||
// Клик по карточкам
|
||
document.querySelectorAll('.calc-card').forEach(card => {
|
||
card.addEventListener('click', () => {
|
||
const type = card.dataset.type;
|
||
const id = card.dataset.id;
|
||
selectOption(type, id);
|
||
});
|
||
});
|
||
}
|
||
|
||
function renderCard(type, id, name, price, selected) {
|
||
const priceText = price === 0 ? 'Бесплатно' : `+${price.toLocaleString()} ₸`;
|
||
return `
|
||
<label class="calc-card kt-ai-card ${selected ? 'selected' : ''}" data-type="${type}" data-id="${id}" style="padding: var(--kt-ai-space-5)">
|
||
<input type="radio" name="${type}" class="radio-hidden" ${selected ? 'checked' : ''}>
|
||
<div style="display: flex; justify-content: space-between; align-items: center">
|
||
<span style="font-weight: var(--kt-ai-weight-medium)">${name}</span>
|
||
<span class="price-badge">${priceText}</span>
|
||
</div>
|
||
</label>
|
||
`;
|
||
}
|
||
|
||
// Выбор опции
|
||
function selectOption(type, id) {
|
||
state[type] = id;
|
||
saveState();
|
||
renderOptions();
|
||
updateTotal();
|
||
logAction();
|
||
}
|
||
|
||
// Расчёт итога
|
||
function updateTotal() {
|
||
let total = 0;
|
||
if (state.tariff) {
|
||
const t = TARIFFS.find(x => x.id === state.tariff);
|
||
total += t.price;
|
||
}
|
||
if (state.speed) {
|
||
const s = SPEEDS.find(x => x.id === state.speed);
|
||
total += s.price;
|
||
}
|
||
if (state.equipment) {
|
||
const e = EQUIPMENT.find(x => x.id === state.equipment);
|
||
total += e.price;
|
||
}
|
||
if (state.discount) {
|
||
total = total * 0.9;
|
||
}
|
||
document.getElementById('totalAmount').textContent = Math.round(total).toLocaleString() + ' ₸';
|
||
}
|
||
|
||
// LocalStorage
|
||
function saveState() {
|
||
localStorage.setItem('calcState', JSON.stringify(state));
|
||
}
|
||
|
||
function loadState() {
|
||
try {
|
||
const saved = localStorage.getItem('calcState');
|
||
if (saved) {
|
||
state = JSON.parse(saved);
|
||
}
|
||
} catch (e) {}
|
||
}
|
||
|
||
// Скидка
|
||
function setupDiscount() {
|
||
const check = document.getElementById('discountCheck');
|
||
check.checked = state.discount;
|
||
check.addEventListener('change', () => {
|
||
state.discount = check.checked;
|
||
saveState();
|
||
updateTotal();
|
||
logAction();
|
||
});
|
||
}
|
||
|
||
// Журнал
|
||
function logAction() {
|
||
const entry = {
|
||
time: new Date().toISOString(),
|
||
tariff: state.tariff,
|
||
speed: state.speed,
|
||
equipment: state.equipment,
|
||
total: calculateTotal()
|
||
};
|
||
let journal = [];
|
||
try {
|
||
const saved = localStorage.getItem('calcJournal');
|
||
if (saved) journal = JSON.parse(saved);
|
||
} catch (e) {}
|
||
journal.unshift(entry);
|
||
if (journal.length > 10) journal = journal.slice(0, 10);
|
||
localStorage.setItem('calcJournal', JSON.stringify(journal));
|
||
renderJournal();
|
||
}
|
||
|
||
function calculateTotal() {
|
||
let total = 0;
|
||
if (state.tariff) total += TARIFFS.find(x => x.id === state.tariff).price;
|
||
if (state.speed) total += SPEEDS.find(x => x.id === state.speed).price;
|
||
if (state.equipment) total += EQUIPMENT.find(x => x.id === state.equipment).price;
|
||
if (state.discount) total = total * 0.9;
|
||
return Math.round(total);
|
||
}
|
||
|
||
function renderJournal() {
|
||
const list = document.getElementById('journalList');
|
||
let journal = [];
|
||
try {
|
||
const saved = localStorage.getItem('calcJournal');
|
||
if (saved) journal = JSON.parse(saved);
|
||
} catch (e) {}
|
||
|
||
if (journal.length === 0) {
|
||
list.innerHTML = '<div class="journal-entry" style="color: var(--kt-ai-fg-muted)">Пока нет записей</div>';
|
||
return;
|
||
}
|
||
|
||
list.innerHTML = journal.map(j => {
|
||
const time = new Date(j.time).toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' });
|
||
const tariffName = TARIFFS.find(t => t.id === j.tariff)?.name || '—';
|
||
const speedName = SPEEDS.find(s => s.id === j.speed)?.name || '—';
|
||
const equipName = EQUIPMENT.find(e => e.id === j.equipment)?.name || '—';
|
||
return `
|
||
<div class="journal-entry">
|
||
<div style="font-weight: var(--kt-ai-weight-medium)">${time} — <strong>${j.total.toLocaleString()} ₸</strong></div>
|
||
<div style="color: var(--kt-ai-fg-muted); font-size: var(--kt-ai-text-xs)">${tariffName} | ${speedName} | ${equipName}</div>
|
||
</div>
|
||
`;
|
||
}).join('');
|
||
}
|
||
|
||
// Копирование
|
||
document.getElementById('copyBtn').addEventListener('click', () => {
|
||
if (!state.tariff || !state.speed || !state.equipment) {
|
||
document.getElementById('copyStatus').textContent = 'Выберите все параметры';
|
||
document.getElementById('copyStatus').style.color = 'var(--kt-ai-danger)';
|
||
return;
|
||
}
|
||
|
||
const t = TARIFFS.find(x => x.id === state.tariff);
|
||
const s = SPEEDS.find(x => x.id === state.speed);
|
||
const e = EQUIPMENT.find(x => x.id === state.equipment);
|
||
const total = calculateTotal();
|
||
|
||
const discountText = state.discount ? '\n🎁 Скидка 10% (сотрудник КТ): применена' : '';
|
||
const text = `📊 Расчёт стоимости подключения:
|
||
|
||
🔹 Тариф: ${t.name} — ${t.price.toLocaleString()} ₸
|
||
🔹 Скорость: ${s.name} — ${s.price === 0 ? 'Бесплатно' : '+' + s.price.toLocaleString() + ' ₸'}
|
||
🔹 Оборудование: ${e.name} — ${e.price === 0 ? 'Бесплатно' : '+' + e.price.toLocaleString() + ' ₸/мес'}${discountText}
|
||
|
||
💰 Итого: ${total.toLocaleString()} ₸ в месяц`;
|
||
|
||
navigator.clipboard.writeText(text).then(() => {
|
||
document.getElementById('copyStatus').textContent = '✓ Скопировано в буфер обмена';
|
||
document.getElementById('copyStatus').style.color = 'var(--kt-ai-fg-muted)';
|
||
setTimeout(() => {
|
||
document.getElementById('copyStatus').textContent = '';
|
||
}, 3000);
|
||
}).catch(() => {
|
||
document.getElementById('copyStatus').textContent = 'Не удалось скопировать';
|
||
document.getElementById('copyStatus').style.color = 'var(--kt-ai-danger)';
|
||
});
|
||
});
|
||
|
||
// Анимация появления
|
||
function setupReveal() {
|
||
const io = new IntersectionObserver(function(entries) {
|
||
entries.forEach(function(e) {
|
||
if (e.isIntersecting) e.target.classList.add('in');
|
||
});
|
||
}, { threshold: 0.15 });
|
||
document.querySelectorAll('.reveal').forEach(function(el) { io.observe(el); });
|
||
}
|
||
|
||
// Старт
|
||
init();
|
||
</script>
|
||
|
||
</body>
|
||
</html>
|