| Форум » Флуд » Общение » Поговорим о... |
| Поговорим о... |
Понедельник, 2025-10-27, 08:44
# 301
Fff
Прикрепления:
8753146.zip
(13.8 Kb)
|
Понедельник, 2025-10-27, 10:10
# 302
444
Прикрепления:
344433.zip
(16.1 Kb)
|
Понедельник, 2025-10-27, 10:25
# 303
<script>
document.addEventListener('DOMContentLoaded', function() { function handleAnchorScroll() { if (window.location.hash) { const element = document.querySelector(window.location.hash); if (element) { const headerHeight = 80; // Подставьте высоту вашего хедера const elementPosition = element.getBoundingClientRect().top; const offsetPosition = elementPosition + window.pageYOffset - headerHeight; window.scrollTo({ top: offsetPosition, behavior: 'smooth' }); } } } // Запускаем сразу handleAnchorScroll(); // И при изменении хеша window.addEventListener('hashchange', handleAnchorScroll); }); </script> |
Понедельник, 2025-10-27, 11:08
# 304
<style>
html { scroll-padding-top: var(--header-height, 80px); scroll-behavior: smooth; } </style> <script> function updateHeaderHeight() { const header = document.querySelector('header, .site-header, nav'); if (header) { const height = header.offsetHeight; document.documentElement.style.setProperty('--header-height', height + 'px'); return height; } return 80; // fallback } document.addEventListener('DOMContentLoaded', function() { // Обновляем высоту при загрузке и ресайзе updateHeaderHeight(); window.addEventListener('resize', updateHeaderHeight); // Обработка якорных ссылок function scrollToAnchor() { if (window.location.hash) { const element = document.querySelector(window.location.hash); if (element) { const headerHeight = updateHeaderHeight(); setTimeout(() => { const y = element.getBoundingClientRect().top + window.pageYOffset - headerHeight; window.scrollTo({ top: y, behavior: 'smooth' }); }, 150); } } } setTimeout(scrollToAnchor, 100); window.addEventListener('hashchange', scrollToAnchor); }); </script> |
Понедельник, 2025-10-27, 11:27
# 305
Понял! Проблема в том, что при изменении масштаба браузера сбиваются расчеты позиций. Браузерные зуммы ломают getBoundingClientRect() и другие методы расчета позиций.
Вот работающее решение: 1. Упрощенный надежный вариант ```html <script> document.addEventListener('DOMContentLoaded', function() { function scrollToAnchor() { if (window.location.hash) { const element = document.querySelector(window.location.hash); if (element) { // Простой и надежный способ - используем встроенный метод // с дополнительной задержкой для применения масштаба setTimeout(() => { element.scrollIntoView({ behavior: 'smooth', block: 'start' }); // Дополнительная корректировка после скролла setTimeout(() => { const header = document.querySelector('header, .header, nav'); if (header) { const headerHeight = header.offsetHeight; if (window.scrollY < element.offsetTop - headerHeight) { window.scrollTo(0, element.offsetTop - headerHeight); } } }, 300); }, 100); } } } // Запускаем с задержкой для стабилизации масштаба setTimeout(scrollToAnchor, 200); window.addEventListener('hashchange', scrollToAnchor); }); </script> ``` 2. Решение с принудительным пересчетом ```html <script> function forceScrollToAnchor() { if (window.location.hash) { const element = document.querySelector(window.location.hash); if (element) { // Принудительно сбрасываем скролл сначала в начало window.scrollTo(0, 0); // Затем скроллим к нужному элементу с учетом хедера setTimeout(() => { const header = document.querySelector('header'); const headerHeight = header ? header.offsetHeight : 80; const targetY = element.offsetTop - headerHeight; window.scrollTo({ top: targetY, behavior: 'smooth' }); // Дублирующая проверка через время setTimeout(() => { const currentPos = window.scrollY; const expectedPos = element.offsetTop - headerHeight; if (Math.abs(currentPos - expectedPos) > 50) { window.scrollTo(0, expectedPos); } }, 400); }, 50); } } } document.addEventListener('DOMContentLoaded', function() { setTimeout(forceScrollToAnchor, 300); window.addEventListener('hashchange', forceScrollToAnchor); }); </script> ``` 3. Решение без smooth scroll (самое надежное) ```html <script> function reliableAnchorScroll() { if (window.location.hash) { // Убираем smooth scroll - он часто ломается при зуме const element = document.querySelector(window.location.hash); if (element) { // Метод 1: используем offsetTop (менее подвержен влиянию масштаба) const header = document.querySelector('header'); const headerHeight = header ? header.offsetHeight : 80; const targetScroll = Math.max(0, element.offsetTop - headerHeight); // Мгновенный скролл window.scrollTo(0, targetScroll); // Проверка и коррекция setTimeout(() => { if (Math.abs(window.scrollY - targetScroll) > 10) { window.scrollTo(0, targetScroll); } }, 100); } } } // Обработчик для всех случаев function initAnchorScroll() { // Ждем полной загрузки и стабилизации setTimeout(reliableAnchorScroll, 400); } document.addEventListener('DOMContentLoaded', initAnchorScroll); window.addEventListener('hashchange', reliableAnchorScroll); window.addEventListener('load', initAnchorScroll); </script> ``` 4. CSS + JS комбо (рекомендую этот) ```css /* Отключаем smooth scroll для якорных ссылок */ html { scroll-behavior: auto; /* или не указывать вообще */ } /* Жесткие отступы для :target */ :target { scroll-margin-top: 100px; /* Задайте с запасом */ } @media (max-width: 1920px) { :target { scroll-margin-top: 80px; } } ``` ```html <script> function zoomResistantAnchor() { if (window.location.hash) { const hash = window.location.hash; const element = document.querySelector(hash); if (element) { // Временно убираем хеш из URL window.location.hash = ""; // Даем время браузеру обработать setTimeout(() => { // Используем самый простой метод element.scrollIntoView(); // Ручная корректировка позиции setTimeout(() => { const header = document.querySelector('header'); if (header) { const currentScroll = window.scrollY; const elementTop = element.getBoundingClientRect().top + currentScroll; const headerHeight = header.offsetHeight; if (currentScroll > elementTop - headerHeight - 10) { window.scrollTo(0, Math.max(0, elementTop - headerHeight)); } } // Возвращаем хеш в URL window.location.hash = hash; }, 50); }, 10); } } } document.addEventListener('DOMContentLoaded', function() { // Большая задержка для стабилизации масштаба setTimeout(zoomResistantAnchor, 500); }); window.addEventListener('hashchange', zoomResistantAnchor); </script> ``` 5. Экстренное решение - убрать все сложности ```html <script> // Максимально простое решение document.addEventListener('DOMContentLoaded', function() { setTimeout(function() { if (window.location.hash) { const element = document.querySelector(window.location.hash); if (element) { // Просто скроллим и все element.scrollIntoView(); // На всякий случай дублируем setTimeout(() => element.scrollIntoView(), 100); } } }, 300); }); </script> ``` Попробуйте сначала вариант 3 - он самый надежный против проблем с масштабом. Если не поможет, то вариант 4. Также проверьте - возможно у вас в CSS есть transform: scale() или другие трансформации, которые влияют на расчет позиций? |
Понедельник, 2025-10-27, 11:48
# 306
<script>
function universalAnchorScroll() { if (window.location.hash) { const element = document.querySelector(window.location.hash); if (element) { // Сначала скроллим просто к элементу element.scrollIntoView(); // Затем корректируем позицию с учетом хедера setTimeout(() => { const header = document.querySelector('header, .header, nav, [class*="header"], [class*="nav"]'); let headerHeight = 80; if (header) { headerHeight = header.offsetHeight; console.log('Found header height:', headerHeight); } // Сильная коррекция - пробуем разные варианты const currentScroll = window.scrollY; const elementRect = element.getBoundingClientRect(); const elementTop = elementRect.top + currentScroll; // Пробуем скролл с разными отступами const targetPosition = elementTop - headerHeight - 20; // дополнительный отступ window.scrollTo(0, targetPosition); // Финальная проверка и коррекция setTimeout(() => { const finalPosition = window.scrollY; if (Math.abs(finalPosition - targetPosition) > 30) { window.scrollTo(0, elementTop - headerHeight - 10); } }, 150); }, 50); } } } document.addEventListener('DOMContentLoaded', function() { setTimeout(universalAnchorScroll, 500); }); window.addEventListener('hashchange', universalAnchorScroll); </script> |
Понедельник, 2025-10-27, 11:49
# 307
Uncaught SyntaxError: Failed to execute 'querySelector' on 'Document': '#%D1%81%D1%82%D1%80%D1%83%D0%BA%D1%82%D1%83%D1%80%D0%B8%D1%80%D0%BE%D0%B2%D0%B0%D0%BD%D0%BD%D0%BE%D0%B5-%D0%BE%D0%BF%D0%B8%D1%81%D0%B0%D0%BD%D0%B8%D0%B5-%D0%B8%D0%BD%D1%82%D0%B5%D1%80%D0%B2%D0%B0%D0%BB%D0%B0' is not a valid selector.
at universalAnchorScroll (main.js:57:28) |
Понедельник, 2025-10-27, 11:52
# 308
<script>
function universalAnchorScroll() { if (window.location.hash) { // Декодируем URL и убираем # для querySelector const hash = decodeURIComponent(window.location.hash.substring(1)); try { const element = document.getElementById(hash); if (element) { // Сначала скроллим просто к элементу element.scrollIntoView(); // Затем корректируем позицию с учетом хедера setTimeout(() => { const header = document.querySelector('header, .header, nav, [class*="header"], [class*="nav"]'); let headerHeight = 80; if (header) { headerHeight = header.offsetHeight; console.log('Found header height:', headerHeight); } // Сильная коррекция const currentScroll = window.scrollY; const elementRect = element.getBoundingClientRect(); const elementTop = elementRect.top + currentScroll; // Пробуем скролл с разными отступами const targetPosition = elementTop - headerHeight - 20; window.scrollTo(0, targetPosition); // Финальная проверка и коррекция setTimeout(() => { const finalPosition = window.scrollY; if (Math.abs(finalPosition - targetPosition) > 30) { window.scrollTo(0, elementTop - headerHeight - 10); } }, 150); }, 50); } else { console.log('Element not found with id:', hash); } } catch (e) { console.error('Error in anchor scroll:', e); } } } document.addEventListener('DOMContentLoaded', function() { setTimeout(universalAnchorScroll, 500); }); window.addEventListener('hashchange', universalAnchorScroll); </script> |
Понедельник, 2025-10-27, 12:01
# 309
Отлично, что работает! А то, что сначала некорректно, а потом выравнивается - это нормально. Это происходит потому, что:
1. Браузер сначала пытается сам проскроллить к якорю 2. Потом наш скрипт исправляет позицию Вот варианты как это улучшить: 1. Ускорить исправление (уменьшить задержки) ```html <script> function fastAnchorScroll() { if (window.location.hash) { const hash = decodeURIComponent(window.location.hash.substring(1)); try { const element = document.getElementById(hash); if (element) { // Убираем начальный scrollIntoView - сразу корректируем const header = document.querySelector('header, .header, nav'); let headerHeight = header ? header.offsetHeight : 80; // Немедленный скролл к правильной позиции const targetPosition = Math.max(0, element.offsetTop - headerHeight - 10); window.scrollTo(0, targetPosition); // Быстрая коррекция через минимальное время setTimeout(() => { window.scrollTo(0, targetPosition); }, 50); // Финальная тонкая настройка setTimeout(() => { const currentPos = window.scrollY; if (Math.abs(currentPos - targetPosition) > 5) { window.scrollTo(0, targetPosition); } }, 150); } } catch (e) { console.error('Error in anchor scroll:', e); } } } // Уменьшаем задержку перед запуском document.addEventListener('DOMContentLoaded', function() { setTimeout(fastAnchorScroll, 100); // было 500, стало 100 }); window.addEventListener('hashchange', fastAnchorScroll); </script> ``` 2. Предотвратить браузерный скролл ```html <script> function preventBrowserAnchorScroll() { if (window.location.hash) { // Временно убираем хеш чтобы браузер не скроллил const hash = window.location.hash; history.replaceState(null, null, ' '); const decodedHash = decodeURIComponent(hash.substring(1)); const element = document.getElementById(decodedHash); if (element) { const header = document.querySelector('header'); const headerHeight = header ? header.offsetHeight : 80; const targetPosition = Math.max(0, element.offsetTop - headerHeight); // Сразу скроллим к правильной позиции window.scrollTo(0, targetPosition); // Возвращаем хеш в URL setTimeout(() => { history.replaceState(null, null, hash); }, 100); } } } document.addEventListener('DOMContentLoaded', function() { setTimeout(preventBrowserAnchorScroll, 50); }); window.addEventListener('hashchange', preventBrowserAnchorScroll); </script> ``` 3. CSS решение для мгновенного применения Добавьте в CSS (если еще нет): ```css /* Отключаем плавный скролл для якорных переходов */ html { scroll-behavior: auto; } /* Скрываем элемент до применения скрипта */ [data-anchor-target] { opacity: 0; } ``` И обновите скрипт: ```html <script> function smoothAnchorScroll() { if (window.location.hash) { const hash = decodeURIComponent(window.location.hash.substring(1)); const element = document.getElementById(hash); if (element) { // Помечаем элемент для CSS element.setAttribute('data-anchor-target', 'true'); const header = document.querySelector('header'); const headerHeight = header ? header.offsetHeight : 80; const targetPosition = Math.max(0, element.offsetTop - headerHeight); // Мгновенный скролл window.scrollTo(0, targetPosition); // Показываем элемент после скролла setTimeout(() => { element.removeAttribute('data-anchor-target'); }, 100); } } } document.addEventListener('DOMContentLoaded', function() { setTimeout(smoothAnchorScroll, 10); // Минимальная задержка }); window.addEventListener('hashchange', smoothAnchorScroll); </script> ``` 4. Оптимальный вариант (рекомендую) ```html <script> function optimalAnchorScroll() { if (window.location.hash) { const hash = window.location.hash; const decodedHash = decodeURIComponent(hash.substring(1)); const element = document.getElementById(decodedHash); if (element) { // Быстрый расчет и скролл const header = document.querySelector('header'); const headerHeight = header ? header.offsetHeight : 80; const targetScroll = Math.max(0, element.offsetTop - headerHeight - 5); // Один точный скролл вместо нескольких requestAnimationFrame(() => { window.scrollTo(0, targetScroll); }); } } } // Запускаем как можно раньше if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', optimalAnchorScroll); } else { optimalAnchorScroll(); } window.addEventListener('hashchange', optimalAnchorScroll); </script> ``` Рекомендую вариант 4 - он использует requestAnimationFrame для максимально быстрого и плавного скролла без видимого "дергания". Если все равно заметно мигание, попробуйте вариант 2 - он полностью предотвращает браузерный скролл. Какой вариант больше подходит? |
Понедельник, 2025-10-27, 16:22
# 311
55555
Прикрепления:
shhshhshhshhsh.zip
(17.6 Kb)
|
Вторник, 2025-10-28, 10:03
# 312
<!DOCTYPE html>
<html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .path-input { width: 100%; padding: 12px 15px; border: 1px solid #ced4da; border-radius: 4px; font-size: 16px; margin-bottom: 15px; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDate = null; let selectedFiles = []; // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { const fileNames = selectedFiles.map(f => f.name).join(', '); fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}. ${fileNames}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; fileContents.forEach(file => { const logs = parseLogFile(file.content, file.name); allLogs = allLogs.concat(logs); }); if (allLogs.length === 0) { throw new Error('Не удалось распознать данные в логах'); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; // Регулярное выражение для парсинга строки лога IIS // Формат: дата время s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken const logRegex = /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+([\d.]+)\s+(\w+)\s+([^\s?]+)(?:\?([^\s]+))?\s+(\d+)\s+([^\s]+)\s+([\d.]+)\s+([^\s]+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/; for (const line of lines) { if (!line.trim()) continue; const match = line.match(logRegex); if (match) { const [ fullMatch, date, time, serverIp, method, uriStem, uriQuery, port, username, clientIp, userAgent, statusCode, subStatus, win32Status, timeTaken ] = match; // Фильтруем только успешные запросы и основные страницы if (statusCode === '200' && !uriStem.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && uriStem !== '/') { logs.push({ date: date, time: time, user: username !== '-' ? username : 'Неизвестно', ip: clientIp, method: method, page: uriStem + (uriQuery ? '?' + uriQuery : ''), statusCode: parseInt(statusCode), filename: filename }); } } } return logs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { filteredLogs = allLogs.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { // Сокращаем длинные названия страниц для легенды return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { // Обновляем заголовок календаря const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; // Получаем уникальные даты с данными const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); // Создаем объект для быстрой проверки наличия данных по дате const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); // Очищаем календарь calendar.innerHTML = ''; // Добавляем заголовки дней недели const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); // Определяем первый и последний день месяца для отображения const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); // Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.) let firstDayOfWeek = firstDayOfMonth.getDay(); // Преобразуем к формату Пн=0, Вт=1, ..., Вс=6 firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Добавляем пустые ячейки для дней предыдущего месяца for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } // Заполняем календарь днями текущего месяца for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; // Добавляем обработчик клика dayElement.addEventListener('click', function() { // Снимаем выделение с предыдущей выбранной даты if (selectedDate) { const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`); if (prevSelected) { prevSelected.classList.remove('selected'); } } // Выделяем новую дату dayElement.classList.add('selected'); selectedDate = dateString; // Устанавливаем фильтр на выбранную дату dateFrom.value = dateString; dateTo.value = dateString; applyFilters(); }); } calendar.appendChild(dayElement); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html> |
Вторник, 2025-10-28, 10:21
# 313
<!DOCTYPE html>
<html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .path-input { width: 100%; padding: 12px 15px; border: 1px solid #ced4da; border-radius: 4px; font-size: 16px; margin-bottom: 15px; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDate = null; let selectedFiles = []; // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; fileContents.forEach(file => { const logs = parseLogFile(file.content, file.name); allLogs = allLogs.concat(logs); }); if (allLogs.length === 0) { throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.'); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; // Регулярное выражение для парсинга строки лога IIS // Формат: дата время s-ip cs-method cs-uri-stem cs-uri-query s-port cs-username c-ip cs(User-Agent) sc-status sc-substatus sc-win32-status time-taken // Пример: 2025-10-27 08:19:27 10.64.241.57 POST /22221123/Reportsu.aspx report=Catalog_Point 80 MYPC\IvanovAA 15.34.19.44 Mozilla/5.0+(Windows+NT+10.0;+Win64;+x64)+Yowser/2.5+Safari/537.36 200 0 0 4232 const logRegex = /^(\d{4}-\d{2}-\d{2})\s+(\d{2}:\d{2}:\d{2})\s+(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\S+)\s+(\S+)\s+(\S+)\s+(\d+)\s+(\d+)\s+(\d+)\s+(\d+)/; for (const line of lines) { if (!line.trim()) continue; const match = line.match(logRegex); if (match) { const [ fullMatch, date, time, serverIp, method, uriStem, uriQuery, port, username, clientIp, userAgent, statusCode, subStatus, win32Status, timeTaken ] = match; // Фильтруем только успешные запросы и основные страницы // Исключаем статические ресурсы (JS, CSS, изображения и т.д.) if (statusCode === '200' && !uriStem.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && uriStem !== '/favicon.ico' && uriStem !== '/') { logs.push({ date: date, time: time, user: username !== '-' ? username : 'Неизвестно', ip: clientIp, method: method, page: uriStem + (uriQuery && uriQuery !== '-' ? '?' + uriQuery : ''), statusCode: parseInt(statusCode), filename: filename }); } } } return logs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { filteredLogs = allLogs.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { // Сокращаем длинные названия страниц для легенды return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { // Обновляем заголовок календаря const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; // Получаем уникальные даты с данными const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); // Создаем объект для быстрой проверки наличия данных по дате const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); // Очищаем календарь calendar.innerHTML = ''; // Добавляем заголовки дней недели const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); // Определяем первый и последний день месяца для отображения const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); // Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.) let firstDayOfWeek = firstDayOfMonth.getDay(); // Преобразуем к формату Пн=0, Вт=1, ..., Вс=6 firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Добавляем пустые ячейки для дней предыдущего месяца for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } // Заполняем календарь днями текущего месяца for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; // Добавляем обработчик клика dayElement.addEventListener('click', function() { // Снимаем выделение с предыдущей выбранной даты if (selectedDate) { const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`); if (prevSelected) { prevSelected.classList.remove('selected'); } } // Выделяем новую дату dayElement.classList.add('selected'); selectedDate = dateString; // Устанавливаем фильтр на выбранную дату dateFrom.value = dateString; dateTo.value = dateString; applyFilters(); }); } calendar.appendChild(dayElement); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html> |
Вторник, 2025-10-28, 10:28
# 314
<!DOCTYPE html>
<html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> </section> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDate = null; let selectedFiles = []; // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; fileContents.forEach(file => { const logs = parseLogFile(file.content, file.name); allLogs = allLogs.concat(logs); }); if (allLogs.length === 0) { throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.'); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов - УПРОЩЕННАЯ ВЕРСИЯ function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; for (const line of lines) { if (!line.trim()) continue; // Простой парсинг по пробелам - берем первые 10 полей const parts = line.split(/\s+/); if (parts.length < 10) continue; try { const date = parts[0]; const time = parts[1]; const ip = parts[2]; const method = parts[3]; const page = parts[4]; const query = parts[5] !== '-' ? parts[5] : ''; const port = parts[6]; const user = parts[7]; const clientIp = parts[8]; const userAgent = parts[9]; const statusCode = parts[10]; // Проверяем, что это валидная запись if (date && time && ip && method && page && statusCode) { // Фильтруем только успешные запросы и основные страницы if (statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i) && page !== '/favicon.ico') { logs.push({ date: date, time: time, user: user !== '-' ? user : 'Неизвестно', ip: clientIp, method: method, page: page + (query ? '?' + query : ''), statusCode: parseInt(statusCode), filename: filename }); } } } catch (e) { console.log('Ошибка парсинга строки:', line); continue; } } return logs; } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { filteredLogs = allLogs.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { // Сокращаем длинные названия страниц для легенды return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { // Обновляем заголовок календаря const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; // Получаем уникальные даты с данными const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); // Создаем объект для быстрой проверки наличия данных по дате const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); // Очищаем календарь calendar.innerHTML = ''; // Добавляем заголовки дней недели const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); // Определяем первый и последний день месяца для отображения const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); // Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.) let firstDayOfWeek = firstDayOfMonth.getDay(); // Преобразуем к формату Пн=0, Вт=1, ..., Вс=6 firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Добавляем пустые ячейки для дней предыдущего месяца for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } // Заполняем календарь днями текущего месяца for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; // Добавляем обработчик клика dayElement.addEventListener('click', function() { // Снимаем выделение с предыдущей выбранной даты if (selectedDate) { const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`); if (prevSelected) { prevSelected.classList.remove('selected'); } } // Выделяем новую дату dayElement.classList.add('selected'); selectedDate = dateString; // Устанавливаем фильтр на выбранную дату dateFrom.value = dateString; dateTo.value = dateString; applyFilters(); }); } calendar.appendChild(dayElement); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html> |
Вторник, 2025-10-28, 10:39
# 315
<!DOCTYPE html>
<html lang="ru"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Статистика посещений сайта</title> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> <style> * { box-sizing: border-box; margin: 0; padding: 0; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; } body { background-color: #f5f7fa; color: #333; line-height: 1.6; padding: 20px; } .container { max-width: 1400px; margin: 0 auto; background-color: white; border-radius: 10px; box-shadow: 0 0 20px rgba(0, 0, 0, 0.1); padding: 25px; } header { text-align: center; margin-bottom: 30px; padding-bottom: 20px; border-bottom: 1px solid #eaeaea; } h1 { color: #2c3e50; margin-bottom: 10px; } .description { color: #7f8c8d; max-width: 800px; margin: 0 auto; } .upload-section { background-color: #f8f9fa; padding: 20px; border-radius: 8px; margin-bottom: 30px; border: 1px dashed #dee2e6; } .upload-label { display: block; margin-bottom: 15px; font-weight: 600; color: #495057; } .btn { background-color: #3498db; color: white; border: none; padding: 12px 25px; border-radius: 4px; cursor: pointer; font-size: 16px; font-weight: 600; transition: background-color 0.3s; } .btn:hover { background-color: #2980b9; } .btn-secondary { background-color: #6c757d; } .btn-secondary:hover { background-color: #5a6268; } .file-input-wrapper { position: relative; display: inline-block; margin-right: 10px; } .file-input { position: absolute; left: 0; top: 0; opacity: 0; width: 100%; height: 100%; cursor: pointer; } .filters { display: flex; flex-wrap: wrap; gap: 15px; margin-bottom: 25px; padding: 15px; background-color: #f8f9fa; border-radius: 8px; } .filter-group { flex: 1; min-width: 200px; } .filter-label { display: block; margin-bottom: 8px; font-weight: 600; color: #495057; } .filter-input, .filter-select { width: 100%; padding: 10px; border: 1px solid #ced4da; border-radius: 4px; } .stats-container { display: grid; grid-template-columns: 1fr 1fr; gap: 25px; margin-bottom: 30px; } .stats-card { background-color: white; border-radius: 8px; padding: 20px; box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } .stats-card h3 { margin-bottom: 15px; color: #2c3e50; border-bottom: 1px solid #eaeaea; padding-bottom: 10px; } .chart-container { height: 300px; margin-top: 10px; } .popular-pages-list { margin-top: 15px; max-height: 250px; overflow-y: auto; } .page-item { display: flex; justify-content: space-between; padding: 8px 10px; border-bottom: 1px solid #f0f0f0; } .page-item:hover { background-color: #f8f9fa; } .table-container { overflow-x: auto; margin-top: 30px; } table { width: 100%; border-collapse: collapse; margin-top: 10px; } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid #eaeaea; } th { background-color: #f8f9fa; font-weight: 600; color: #495057; } tr:hover { background-color: #f8f9fa; } .loading { text-align: center; padding: 20px; display: none; } .error { color: #e74c3c; padding: 10px; background-color: #fadbd8; border-radius: 4px; margin-top: 10px; display: none; } .debug { color: #666; padding: 10px; background-color: #f8f9fa; border-radius: 4px; margin-top: 10px; font-family: monospace; font-size: 12px; max-height: 200px; overflow-y: auto; display: none; } .calendar-container { margin-top: 30px; padding: 20px; background-color: #f8f9fa; border-radius: 8px; } .calendar-header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 15px; } .calendar-title { font-weight: 600; color: #495057; font-size: 18px; } .calendar-nav { display: flex; gap: 10px; } .calendar-nav-btn { background-color: #3498db; color: white; border: none; padding: 8px 12px; border-radius: 4px; cursor: pointer; font-weight: 600; } .calendar { display: grid; grid-template-columns: repeat(7, 1fr); gap: 5px; } .calendar-day { padding: 8px; text-align: center; border-radius: 4px; background-color: #e9ecef; cursor: pointer; transition: all 0.2s; } .calendar-day.has-data { background-color: #3498db; color: white; font-weight: bold; } .calendar-day.has-data:hover { background-color: #2980b9; transform: scale(1.05); } .calendar-day.selected { background-color: #e74c3c; color: white; } .calendar-day.other-month { background-color: #f8f9fa; color: #adb5bd; } .calendar-day-header { padding: 8px; text-align: center; font-weight: bold; background-color: #dee2e6; } .period-selector { display: flex; gap: 10px; margin-bottom: 15px; } .file-info { margin-top: 10px; padding: 10px; background-color: #e9ecef; border-radius: 4px; font-size: 14px; } @media (max-width: 768px) { .stats-container { grid-template-columns: 1fr; } .filters { flex-direction: column; } .period-selector { flex-direction: column; } } </style> </head> <body> <div class="container"> <header> <h1>Статистика посещений сайта</h1> <p class="description">Загрузите логи IIS 10.0 для анализа посещений. Система автоматически отфильтрует запросы к JS, CSS и другим ресурсам, оставив только основные страницы.</p> </header> <section class="upload-section"> <label class="upload-label">Выберите файлы логов или папку:</label> <div class="file-input-wrapper"> <button class="btn">Выбрать файлы</button> <input type="file" id="logFiles" class="file-input" multiple accept=".log,.txt"> </div> <div class="file-input-wrapper"> <button class="btn btn-secondary">Выбрать папку</button> <input type="file" id="logFolder" class="file-input" webkitdirectory directory multiple> </div> <button id="loadLogs" class="btn">Загрузить логи</button> <button id="clearCache" class="btn btn-secondary">Очистить кеш</button> <button id="debugBtn" class="btn btn-secondary">Отладка</button> <div id="fileInfo" class="file-info"></div> <div id="loading" class="loading">Загрузка и анализ логов...</div> <div id="error" class="error"></div> <div id="debug" class="debug"></div> </section> <section class="filters"> <div class="filter-group"> <label class="filter-label" for="dateFrom">Период с:</label> <input type="date" id="dateFrom" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="dateTo">Период по:</label> <input type="date" id="dateTo" class="filter-input"> </div> <div class="filter-group"> <label class="filter-label" for="userFilter">Фильтр по пользователю:</label> <input type="text" id="userFilter" class="filter-input" placeholder="Введите имя пользователя"> </div> <div class="filter-group"> <label class="filter-label" for="pageFilter">Фильтр по странице:</label> <input type="text" id="pageFilter" class="filter-input" placeholder="Введите часть URL"> </div> </section> <div class="period-selector"> <div class="filter-group"> <label class="filter-label" for="groupBy">Группировать по:</label> <select id="groupBy" class="filter-select"> <option value="day">Дням</option> <option value="month">Месяцам</option> <option value="year">Годам</option> </select> </div> </div> <div class="stats-container"> <div class="stats-card"> <h3>Посещения по датам</h3> <div class="chart-container"> <canvas id="visitsByDateChart"></canvas> </div> </div> <div class="stats-card"> <h3>Топ посещаемых страниц</h3> <div class="chart-container"> <canvas id="popularPagesChart"></canvas> </div> <div class="popular-pages-list" id="popularPagesList"> <!-- Список популярных страниц будет здесь --> </div> </div> </div> <div class="calendar-container"> <div class="calendar-header"> <button id="prevMonth" class="calendar-nav-btn">←</button> <h3 class="calendar-title" id="calendarTitle">Октябрь 2025</h3> <button id="nextMonth" class="calendar-nav-btn">→</button> </div> <div class="calendar" id="calendar"> <!-- Календарь будет сгенерирован здесь --> </div> </div> <div class="table-container"> <h3>Детальная статистика посещений</h3> <table id="statsTable"> <thead> <tr> <th>Дата</th> <th>Время</th> <th>Пользователь</th> <th>IP-адрес</th> <th>Метод</th> <th>Страница</th> <th>Код ответа</th> </tr> </thead> <tbody id="statsTableBody"> <!-- Данные будут загружены здесь --> </tbody> </table> </div> </div> <script> document.addEventListener('DOMContentLoaded', function() { const loadLogsBtn = document.getElementById('loadLogs'); const logFilesInput = document.getElementById('logFiles'); const logFolderInput = document.getElementById('logFolder'); const clearCacheBtn = document.getElementById('clearCache'); const debugBtn = document.getElementById('debugBtn'); const loadingDiv = document.getElementById('loading'); const errorDiv = document.getElementById('error'); const debugDiv = document.getElementById('debug'); const fileInfoDiv = document.getElementById('fileInfo'); const statsTableBody = document.getElementById('statsTableBody'); const dateFrom = document.getElementById('dateFrom'); const dateTo = document.getElementById('dateTo'); const userFilter = document.getElementById('userFilter'); const pageFilter = document.getElementById('pageFilter'); const groupBy = document.getElementById('groupBy'); const popularPagesList = document.getElementById('popularPagesList'); const calendar = document.getElementById('calendar'); const calendarTitle = document.getElementById('calendarTitle'); const prevMonthBtn = document.getElementById('prevMonth'); const nextMonthBtn = document.getElementById('nextMonth'); let allLogs = []; let filteredLogs = []; let dateRange = {}; let currentCalendarDate = new Date(); let selectedDate = null; let selectedFiles = []; let debugMode = false; // Инициализация графиков const visitsByDateChart = new Chart( document.getElementById('visitsByDateChart'), { type: 'bar', data: { labels: [], datasets: [{ label: 'Количество посещений', data: [], backgroundColor: 'rgba(54, 162, 235, 0.5)', borderColor: 'rgba(54, 162, 235, 1)', borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, scales: { y: { beginAtZero: true } } } } ); const popularPagesChart = new Chart( document.getElementById('popularPagesChart'), { type: 'pie', data: { labels: [], datasets: [{ data: [], backgroundColor: [ 'rgba(255, 99, 132, 0.5)', 'rgba(54, 162, 235, 0.5)', 'rgba(255, 206, 86, 0.5)', 'rgba(75, 192, 192, 0.5)', 'rgba(153, 102, 255, 0.5)', 'rgba(255, 159, 64, 0.5)', 'rgba(199, 199, 199, 0.5)' ], borderWidth: 1 }] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'right', } } } } ); // Переключение режима отладки debugBtn.addEventListener('click', function() { debugMode = !debugMode; debugDiv.style.display = debugMode ? 'block' : 'none'; debugBtn.textContent = debugMode ? 'Скрыть отладку' : 'Отладка'; if (debugMode && selectedFiles.length > 0) { showDebugInfo(); } }); // Загрузка кешированных данных function loadCachedData() { const cachedLogs = localStorage.getItem('iisLogsData'); const cachedPath = localStorage.getItem('iisLogsPath'); if (cachedLogs && cachedPath) { allLogs = JSON.parse(cachedLogs); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); fileInfoDiv.textContent = `Загружены кешированные данные из: ${cachedPath}`; } } // Сохранение данных в кеш function saveToCache(data, path) { localStorage.setItem('iisLogsData', JSON.stringify(data)); localStorage.setItem('iisLogsPath', path); } // Очистка кеша clearCacheBtn.addEventListener('click', function() { localStorage.removeItem('iisLogsData'); localStorage.removeItem('iisLogsPath'); allLogs = []; filteredLogs = []; statsTableBody.innerHTML = ''; fileInfoDiv.textContent = ''; visitsByDateChart.data.labels = []; visitsByDateChart.data.datasets[0].data = []; visitsByDateChart.update(); popularPagesChart.data.labels = []; popularPagesChart.data.datasets[0].data = []; popularPagesChart.update(); popularPagesList.innerHTML = ''; calendar.innerHTML = ''; alert('Кеш очищен'); }); // Обработка выбора файлов logFilesInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обработка выбора папки logFolderInput.addEventListener('change', function(e) { selectedFiles = Array.from(e.target.files); updateFileInfo(); }); // Обновление информации о выбранных файлах function updateFileInfo() { if (selectedFiles.length > 0) { fileInfoDiv.textContent = `Выбрано файлов: ${selectedFiles.length}`; } else { fileInfoDiv.textContent = ''; } } // Загрузка логов loadLogsBtn.addEventListener('click', function() { if (selectedFiles.length === 0) { showError('Пожалуйста, выберите файлы логов или папку'); return; } loadingDiv.style.display = 'block'; errorDiv.style.display = 'none'; debugDiv.innerHTML = ''; // Чтение файлов const fileReaders = []; selectedFiles.forEach(file => { const reader = new FileReader(); fileReaders.push(new Promise((resolve) => { reader.onload = function(e) { resolve({ name: file.name, content: e.target.result }); }; reader.readAsText(file); })); }); Promise.all(fileReaders) .then(fileContents => { try { // Парсинг логов из всех файлов allLogs = []; let debugInfo = []; fileContents.forEach(file => { const result = parseLogFile(file.content, file.name); allLogs = allLogs.concat(result.logs); debugInfo = debugInfo.concat(result.debug); }); if (allLogs.length === 0) { throw new Error('Не удалось распознать данные в логах. Проверьте формат файлов.'); } // Сохраняем в кеш const path = selectedFiles.length === 1 ? selectedFiles[0].name : `Папка с ${selectedFiles.length} файлами`; saveToCache(allLogs, path); dateRange = getDateRange(allLogs); setDateFilters(dateRange); currentCalendarDate = new Date(dateRange.max); updateCalendar(); applyFilters(); loadingDiv.style.display = 'none'; // Показываем отладочную информацию if (debugMode) { debugDiv.innerHTML = debugInfo.join('<br>'); } } catch (error) { loadingDiv.style.display = 'none'; showError('Ошибка при обработке логов: ' + error.message); } }) .catch(error => { loadingDiv.style.display = 'none'; showError('Ошибка при чтении файлов: ' + error.message); }); }); // Парсинг файла логов - УПРОЩЕННАЯ ВЕРСИЯ function parseLogFile(content, filename) { const lines = content.split('\n'); const logs = []; const debug = [`Файл: ${filename}`, `Всего строк: ${lines.length}`]; let parsedCount = 0; let sampleLines = []; for (let i = 0; i < lines.length; i++) { const line = lines[i].trim(); if (!line) continue; // Сохраняем несколько первых строк для отладки if (i < 5) { sampleLines.push(`Строка ${i+1}: ${line.substring(0, 100)}...`); } try { // Простой парсинг по пробелам const parts = line.split(/\s+/); // Ищем дату в формате ГГГГ-ММ-ДД let dateIndex = -1; for (let j = 0; j < parts.length; j++) { if (parts[j].match(/^\d{4}-\d{2}-\d{2}$/)) { dateIndex = j; break; } } if (dateIndex === -1) continue; const date = parts[dateIndex]; const time = parts[dateIndex + 1] || ''; const ip = parts[dateIndex + 2] || ''; const method = parts[dateIndex + 3] || ''; const page = parts[dateIndex + 4] || ''; // Ищем статус код (3 цифры) let statusCode = ''; for (let j = dateIndex + 5; j < parts.length; j++) { if (parts[j].match(/^\d{3}$/)) { statusCode = parts[j]; break; } } // Ищем пользователя (содержит обратный слеш) let user = 'Неизвестно'; for (let j = dateIndex + 5; j < parts.length; j++) { if (parts[j].includes('\\')) { user = parts[j]; break; } } if (date && time && statusCode === '200' && !page.match(/\.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$/i)) { logs.push({ date: date, time: time, user: user, ip: ip, method: method, page: page, statusCode: parseInt(statusCode), filename: filename }); parsedCount++; } } catch (e) { // Пропускаем строки с ошибками continue; } } debug.push(...sampleLines); debug.push(`Успешно распарсено: ${parsedCount} записей`); return { logs, debug }; } function showDebugInfo() { if (selectedFiles.length > 0) { const file = selectedFiles[0]; const reader = new FileReader(); reader.onload = function(e) { const content = e.target.result; const lines = content.split('\n'); let debugInfo = [ `Файл: ${file.name}`, `Размер: ${file.size} байт`, `Строк: ${lines.length}`, 'Первые 5 строк:' ]; for (let i = 0; i < Math.min(5, lines.length); i++) { debugInfo.push(`[${i+1}] ${lines[i].substring(0, 200)}`); } debugDiv.innerHTML = debugInfo.join('<br>'); }; reader.readAsText(file); } } // Применение фильтров [dateFrom, dateTo, userFilter, pageFilter, groupBy].forEach(filter => { filter.addEventListener('change', applyFilters); }); userFilter.addEventListener('input', applyFilters); pageFilter.addEventListener('input', applyFilters); // Навигация по календарю prevMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() - 1); updateCalendar(); }); nextMonthBtn.addEventListener('click', function() { currentCalendarDate.setMonth(currentCalendarDate.getMonth() + 1); updateCalendar(); }); function applyFilters() { filteredLogs = allLogs.filter(log => { // Фильтр по дате const logDate = new Date(log.date); const fromDate = dateFrom.value ? new Date(dateFrom.value) : new Date(dateRange.min); const toDate = dateTo.value ? new Date(dateTo.value) : new Date(dateRange.max); const dateMatch = logDate >= fromDate && logDate <= toDate; // Фильтр по пользователю const userMatch = !userFilter.value || log.user.toLowerCase().includes(userFilter.value.toLowerCase()); // Фильтр по странице const pageMatch = !pageFilter.value || log.page.toLowerCase().includes(pageFilter.value.toLowerCase()); return dateMatch && userMatch && pageMatch; }); updateStatsTable(); updateCharts(); updatePopularPagesList(); } function updateStatsTable() { statsTableBody.innerHTML = ''; filteredLogs.forEach(log => { const row = document.createElement('tr'); const dateCell = document.createElement('td'); dateCell.textContent = log.date; row.appendChild(dateCell); const timeCell = document.createElement('td'); timeCell.textContent = log.time; row.appendChild(timeCell); const userCell = document.createElement('td'); userCell.textContent = log.user; row.appendChild(userCell); const ipCell = document.createElement('td'); ipCell.textContent = log.ip; row.appendChild(ipCell); const methodCell = document.createElement('td'); methodCell.textContent = log.method; row.appendChild(methodCell); const pageCell = document.createElement('td'); pageCell.textContent = log.page; row.appendChild(pageCell); const codeCell = document.createElement('td'); codeCell.textContent = log.statusCode; row.appendChild(codeCell); statsTableBody.appendChild(row); }); } function updateCharts() { // Обновление графика по датам с группировкой const visitsByPeriod = {}; filteredLogs.forEach(log => { let periodKey; const date = new Date(log.date); switch(groupBy.value) { case 'month': periodKey = `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}`; break; case 'year': periodKey = `${date.getFullYear()}`; break; default: // day periodKey = log.date; } visitsByPeriod[periodKey] = (visitsByPeriod[periodKey] || 0) + 1; }); const periods = Object.keys(visitsByPeriod).sort(); const visits = periods.map(period => visitsByPeriod[period]); visitsByDateChart.data.labels = periods; visitsByDateChart.data.datasets[0].data = visits; visitsByDateChart.update(); // Обновление графика популярных страниц const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 7); popularPagesChart.data.labels = sortedPages.map(item => { // Сокращаем длинные названия страниц для легенды return item[0].length > 30 ? item[0].substring(0, 30) + '...' : item[0]; }); popularPagesChart.data.datasets[0].data = sortedPages.map(item => item[1]); popularPagesChart.update(); } function updatePopularPagesList() { const pageCounts = {}; filteredLogs.forEach(log => { pageCounts[log.page] = (pageCounts[log.page] || 0) + 1; }); const sortedPages = Object.entries(pageCounts) .sort((a, b) => b[1] - a[1]) .slice(0, 10); popularPagesList.innerHTML = ''; sortedPages.forEach(([page, count]) => { const pageItem = document.createElement('div'); pageItem.className = 'page-item'; const pageName = document.createElement('span'); pageName.textContent = page; const pageCount = document.createElement('span'); pageCount.textContent = count; pageCount.style.fontWeight = 'bold'; pageCount.style.color = '#3498db'; pageItem.appendChild(pageName); pageItem.appendChild(pageCount); popularPagesList.appendChild(pageItem); }); } function updateCalendar() { // Обновляем заголовок календаря const monthNames = ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь"]; calendarTitle.textContent = `${monthNames[currentCalendarDate.getMonth()]} ${currentCalendarDate.getFullYear()}`; // Получаем уникальные даты с данными const datesWithData = [...new Set(allLogs.map(log => log.date))].sort(); // Создаем объект для быстрой проверки наличия данных по дате const dataDates = {}; datesWithData.forEach(date => { dataDates[date] = true; }); // Очищаем календарь calendar.innerHTML = ''; // Добавляем заголовки дней недели const daysOfWeek = ['Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб', 'Вс']; daysOfWeek.forEach(day => { const dayHeader = document.createElement('div'); dayHeader.className = 'calendar-day-header'; dayHeader.textContent = day; calendar.appendChild(dayHeader); }); // Определяем первый и последний день месяца для отображения const firstDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth(), 1); const lastDayOfMonth = new Date(currentCalendarDate.getFullYear(), currentCalendarDate.getMonth() + 1, 0); // Определяем день недели первого дня месяца (0 - воскресенье, 1 - понедельник и т.д.) let firstDayOfWeek = firstDayOfMonth.getDay(); // Преобразуем к формату Пн=0, Вт=1, ..., Вс=6 firstDayOfWeek = firstDayOfWeek === 0 ? 6 : firstDayOfWeek - 1; // Добавляем пустые ячейки для дней предыдущего месяца for (let i = 0; i < firstDayOfWeek; i++) { const emptyDay = document.createElement('div'); emptyDay.className = 'calendar-day other-month'; calendar.appendChild(emptyDay); } // Заполняем календарь днями текущего месяца for (let day = 1; day <= lastDayOfMonth.getDate(); day++) { const dayElement = document.createElement('div'); dayElement.className = 'calendar-day'; const dateString = `${currentCalendarDate.getFullYear()}-${String(currentCalendarDate.getMonth() + 1).padStart(2, '0')}-${String(day).padStart(2, '0')}`; dayElement.textContent = day; dayElement.dataset.date = dateString; if (dataDates[dateString]) { dayElement.classList.add('has-data'); dayElement.title = `Есть данные за ${dateString}`; // Добавляем обработчик клика dayElement.addEventListener('click', function() { // Снимаем выделение с предыдущей выбранной даты if (selectedDate) { const prevSelected = document.querySelector(`.calendar-day[data-date="${selectedDate}"]`); if (prevSelected) { prevSelected.classList.remove('selected'); } } // Выделяем новую дату dayElement.classList.add('selected'); selectedDate = dateString; // Устанавливаем фильтр на выбранную дату dateFrom.value = dateString; dateTo.value = dateString; applyFilters(); }); } calendar.appendChild(dayElement); } } function getDateRange(logs) { const dates = logs.map(log => new Date(log.date)); const minDate = new Date(Math.min(...dates)); const maxDate = new Date(Math.max(...dates)); return { min: minDate.toISOString().split('T')[0], max: maxDate.toISOString().split('T')[0] }; } function setDateFilters(dateRange) { dateFrom.value = dateRange.min; dateTo.value = dateRange.max; } function showError(message) { errorDiv.textContent = message; errorDiv.style.display = 'block'; } // Загружаем кешированные данные при запуске loadCachedData(); }); </script> </body> </html> |
| |||


