/
Zeylxxddd
/
IPUP
Обзор
Документация
Войти
/
Zeylxxddd
/
IPUP
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
Frontend/HTML/map.html
404 строки
17 KB
Zeylxxddd
update: map.html
26 май 2026, 19:05
Верифицирован
26 май 2026, 19:05
21aa170
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="main.css"> <link rel="stylesheet" href="form.css"> <link rel="stylesheet" href="map.css"> <title>iPUP – Карта ПВЗ</title> <style> /* Дополнительные стили для новых элементов */ .filter-panel { background: white; padding: 15px; border-radius: 10px; margin-bottom: 20px; box-shadow: 0 2px 5px rgba(0,0,0,0.1); } .filter-panel select, .filter-panel button { padding: 8px 12px; font-size: 1rem; margin-right: 10px; border-radius: 6px; border: 1px solid #ccc; cursor: pointer; } .filter-panel button { background-color: #333; color: white; border: none; } .filter-panel button:hover { background-color: #555; } .kpi-box { background: #f8f9fa; padding: 15px; border-radius: 10px; margin-bottom: 20px; display: flex; justify-content: space-between; flex-wrap: wrap; gap: 15px; } .kpi-item { flex: 1; text-align: center; background: white; padding: 10px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); } .kpi-value { font-size: 1.8rem; font-weight: bold; color: #333; } .kpi-label { font-size: 0.9rem; color: #666; } .overload-cell { background-color: #ffcccc !important; color: #d32f2f; font-weight: bold; } .stats-table-container { max-height: 400px; overflow-y: auto; } #stats-table th, #stats-table td { padding: 8px; text-align: center; } </style> </head> <body> <header class="header"> <div class="logo"> <img src="logoimg.png" alt="Логотип" class="logo-img"> <span class="logo-text">iPUP</span> </div> <nav class="nav"> <ul class="nav-list"> <li><a href="/">Главная</a></li> <li class="active"><a href="/map">Карта ПВЗ</a></li> <li><a href="/login">Вход/Регистрация</a></li> </ul> </nav> </header> <main class="wb-map-container"> <!-- Левая колонка --> <div class="sidebar"> <!-- Список ПВЗ (режим списка) --> <div class="pvz-list"> <h3>Выберите пункт выдачи</h3> <div id="buttons-container"></div> </div> <!-- Блок детальной информации + статистика --> <div class="pvz-details"> <div class="details-header"> <button class="back-button"><span class="arrow">←</span> Назад</button> </div> <div class="details-content"> <p><strong>Адрес:</strong> <span id="detail-address"></span></p> <p><strong>Пропускная способность:</strong> <span id="detail-capacity"></span> оп/час</p> <!-- Фильтр по типу операций --> <div class="filter-panel"> <label>Тип операций: </label> <select id="operation-type"> <option value="all">Все операции</option> <option value="in">Приём (in)</option> <option value="out">Выдача (out)</option> <option value="return">Возврат (return)</option> </select> <button id="export-csv-btn">📥 Экспорт CSV</button> </div> <!-- Блок KPI --> <div class="kpi-box" id="kpi-container"> <div class="kpi-item"> <div class="kpi-value" id="total-ops">-</div> <div class="kpi-label">Всего операций за день</div> </div> <div class="kpi-item"> <div class="kpi-value" id="avg-load">-</div> <div class="kpi-label">Средняя загрузка (%)</div> </div> <div class="kpi-item"> <div class="kpi-value" id="overload-ratio">-</div> <div class="kpi-label">Доля перегруженных часов (%)</div> </div> <div class="kpi-item"> <div class="kpi-value" id="peak-load">-</div> <div class="kpi-label">Пиковая загрузка (%)</div> </div> </div> <!-- Блок статистики --> <h4>Почасовая загрузка</h4> <div class="weekdays"> <button data-day="2026-04-06">Пн</button> <button data-day="2026-04-07">Вт</button> <button data-day="2026-04-08">Ср</button> <button data-day="2026-04-09">Чт</button> <button data-day="2026-04-10">Пт</button> <button data-day="2026-04-11">Сб</button> <button data-day="2026-04-12">Вс</button> </div> <div class="stats-table-container"> <table id="stats-table"> <thead> <tr><th>Время</th><th>Операций</th><th>Загрузка %</th><th>Статус</th></tr> </thead> <tbody> <tr><td colspan="4">Выберите пункт и день</td></tr> </tbody> </table> </div> <div id="stats-summary" style="margin-top: 15px; font-size: 0.9rem;"></div> </div> </div> </div> <!-- Правая колонка с картой --> <div class="map-area"> <div id="yandex-map" style="width: 100%; height: 100%; border-radius: 10px;"></div> </div> </main> <script src="https://api-maps.yandex.ru/2.1/?apikey=7dfe176f-e988-4ae3-b782-bc9ac3de7733&lang=ru_RU" type="text/javascript"></script> <script> ymaps.ready(init); let map; let currentPointId = null; let currentDate = '2026-04-06'; let currentType = 'all'; async function init() { // 1. Загружаем пункты с сервера let points = []; try { const response = await fetch('/api/pickup-points'); points = await response.json(); if (!points.length) { document.getElementById('buttons-container').innerHTML = '<p>Нет доступных пунктов</p>'; return; } } catch (err) { console.error('Ошибка загрузки ПВЗ:', err); document.getElementById('buttons-container').innerHTML = '<p>Ошибка загрузки данных</p>'; return; } // 2. Генерируем кнопки ПВЗ const container = document.getElementById('buttons-container'); container.innerHTML = ''; points.forEach(point => { const btn = document.createElement('button'); btn.className = 'pvz-button'; btn.textContent = point.address; btn.dataset.id = point.id; btn.dataset.address = point.address; btn.dataset.capacity = point.capacity_per_hour; btn.dataset.lat = point.latitude; btn.dataset.lon = point.longitude; container.appendChild(btn); }); // 3. Создаём карту const centerLat = points.reduce((sum, p) => sum + parseFloat(p.latitude), 0) / points.length; const centerLon = points.reduce((sum, p) => sum + parseFloat(p.longitude), 0) / points.length; map = new ymaps.Map('yandex-map', { center: [centerLat, centerLon], zoom: 11, controls: ['zoomControl'] }); // 4. Добавляем метки points.forEach((point, idx) => { const placemark = new ymaps.Placemark([point.latitude, point.longitude], { hintContent: point.address, balloonContent: point.address, iconContent: (idx + 1).toString() }, { preset: 'islands#violetIcon' }); map.geoObjects.add(placemark); }); // 5. DOM-элементы const pvzList = document.querySelector('.pvz-list'); const detailsBlock = document.querySelector('.pvz-details'); const detailAddressSpan = document.getElementById('detail-address'); const detailCapacitySpan = document.getElementById('detail-capacity'); const backButton = document.querySelector('.back-button'); const statsTableBody = document.querySelector('#stats-table tbody'); const dayButtons = document.querySelectorAll('.weekdays button'); const typeSelect = document.getElementById('operation-type'); const exportBtn = document.getElementById('export-csv-btn'); // Функция загрузки статистики async function loadStats(pointId, date, type) { if (!pointId) return; try { const url = `/api/stats/daily/${pointId}?date=${date}&type=${type}`; const response = await fetch(url); if (!response.ok) throw new Error('Ошибка загрузки'); const data = await response.json(); renderStatsTable(data.stats, data.capacity_per_hour); updateKPI(data.summary); return data; } catch (err) { console.error(err); statsTableBody.innerHTML = '<tr><td colspan="4">Ошибка загрузки статистики</td></tr>'; document.getElementById('total-ops').innerText = '-'; document.getElementById('avg-load').innerText = '-'; document.getElementById('overload-ratio').innerText = '-'; document.getElementById('peak-load').innerText = '-'; } } function renderStatsTable(stats, capacity) { if (!stats || stats.length === 0) { statsTableBody.innerHTML = '<tr><td colspan="4">Нет данных за этот день</td></tr>'; return; } let html = ''; for (let i = 9; i <= 20; i++) { const stat = stats.find(s => s.hour === i) || { ops: 0, load: 0, overload: false }; const overloadClass = stat.overload ? 'overload-cell' : ''; html += `<tr> <td>${i}:00–${i+1}:00</td> <td class="${overloadClass}">${stat.ops}</td> <td class="${overloadClass}">${stat.load}%</td> <td class="${overloadClass}">${stat.overload ? '⚠️ Перегрузка' : '✅ Норма'}</td> </tr>`; } statsTableBody.innerHTML = html; } function updateKPI(summary) { if (summary) { document.getElementById('total-ops').innerText = summary.total_ops; document.getElementById('avg-load').innerText = summary.avg_load + '%'; document.getElementById('overload-ratio').innerText = summary.overload_hours_ratio + '%'; document.getElementById('peak-load').innerText = summary.peak_hour_load + '%'; } else { document.getElementById('total-ops').innerText = '-'; document.getElementById('avg-load').innerText = '-'; document.getElementById('overload-ratio').innerText = '-'; document.getElementById('peak-load').innerText = '-'; } } // Экспорт CSV async function exportCSV() { if (!currentPointId) { alert('Сначала выберите ПВЗ'); return; } const url = `/api/stats/export?date=${currentDate}&type=${currentType}&pvz_id=${currentPointId}`; window.location.href = url; } // Обработчик смены типа операции typeSelect.addEventListener('change', async () => { currentType = typeSelect.value; if (currentPointId) { await loadStats(currentPointId, currentDate, currentType); } }); exportBtn.addEventListener('click', exportCSV); // Обработка кнопок дней dayButtons.forEach(btn => { btn.addEventListener('click', async () => { const selectedDate = btn.dataset.day; if (!selectedDate || !currentPointId) return; currentDate = selectedDate; dayButtons.forEach(b => b.classList.remove('active')); btn.classList.add('active'); await loadStats(currentPointId, currentDate, currentType); }); }); // Обработка клика по кнопке ПВЗ const buttons = document.querySelectorAll('.pvz-button'); buttons.forEach(button => { button.addEventListener('click', async () => { const address = button.dataset.address; const capacity = button.dataset.capacity; const lat = parseFloat(button.dataset.lat); const lon = parseFloat(button.dataset.lon); const pointId = parseInt(button.dataset.id); detailAddressSpan.textContent = address; detailCapacitySpan.textContent = capacity; if (!isNaN(lat) && !isNaN(lon)) { map.setCenter([lat, lon], 16); } currentPointId = pointId; currentDate = '2026-04-06'; // сброс на понедельник currentType = typeSelect.value; // Активируем кнопку понедельника dayButtons.forEach(btn => { if (btn.dataset.day === currentDate) btn.classList.add('active'); else btn.classList.remove('active'); }); await loadStats(currentPointId, currentDate, currentType); pvzList.style.display = 'none'; detailsBlock.style.display = 'flex'; }); }); // Кнопка "Назад" backButton.addEventListener('click', () => { pvzList.style.display = 'flex'; detailsBlock.style.display = 'none'; map.setCenter([centerLat, centerLon], 11); statsTableBody.innerHTML = '<tr><td colspan="4">Выберите пункт и день</td></tr>'; document.getElementById('total-ops').innerText = '-'; document.getElementById('avg-load').innerText = '-'; document.getElementById('overload-ratio').innerText = '-'; document.getElementById('peak-load').innerText = '-'; dayButtons.forEach(btn => btn.classList.remove('active')); currentPointId = null; }); } // Обновление навигации (авторизация) async function updateNav() { try { const response = await fetch('/api/me'); const data = await response.json(); const navList = document.querySelector('.nav-list'); if (!navList) return; const loginItem = navList.querySelector('li:last-child'); if (data.authenticated) { if (loginItem) { loginItem.innerHTML = '<a href="/logout">Выйти</a>'; } } else { if (loginItem && loginItem.innerHTML !== '<a href="/login">Вход/Регистрация</a>') { loginItem.innerHTML = '<a href="/login">Вход/Регистрация</a>'; } } } catch (err) { console.error('Ошибка проверки авторизации:', err); } } document.addEventListener('DOMContentLoaded', updateNav); </script> </body> </html>