Spaces:
Running
Running
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>日志管理</title> | |
| <style> | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| min-height: 100vh; | |
| padding: 20px; | |
| } | |
| .container { | |
| /*max-width: 1200px;*/ | |
| margin: 0 auto; | |
| background: white; | |
| border-radius: 15px; | |
| box-shadow: 0 20px 40px rgba(0,0,0,0.1); | |
| overflow: hidden; | |
| } | |
| .header { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 30px; | |
| text-align: center; | |
| } | |
| .header h1 { | |
| font-size: 2.5rem; | |
| margin-bottom: 10px; | |
| font-weight: 300; | |
| } | |
| .header p { | |
| opacity: 0.9; | |
| font-size: 1.1rem; | |
| } | |
| .controls { | |
| padding: 30px; | |
| background: #f8f9fa; | |
| border-bottom: 1px solid #e9ecef; | |
| } | |
| .search-container { | |
| display: flex; | |
| gap: 15px; | |
| align-items: center; | |
| flex-wrap: wrap; | |
| } | |
| .search-input { | |
| flex: 1; | |
| min-width: 250px; | |
| padding: 12px 20px; | |
| border: 2px solid #e9ecef; | |
| border-radius: 25px; | |
| font-size: 16px; | |
| transition: all 0.3s ease; | |
| } | |
| .search-input:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .btn { | |
| padding: 12px 25px; | |
| border: none; | |
| border-radius: 25px; | |
| cursor: pointer; | |
| font-size: 16px; | |
| font-weight: 500; | |
| transition: all 0.3s ease; | |
| text-decoration: none; | |
| display: inline-block; | |
| } | |
| .btn-primary { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| } | |
| .btn-primary:hover { | |
| transform: translateY(-2px); | |
| box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4); | |
| } | |
| .btn-secondary { | |
| background: #6c757d; | |
| color: white; | |
| } | |
| .btn-secondary:hover { | |
| background: #5a6268; | |
| transform: translateY(-2px); | |
| } | |
| .page-size-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| margin-top: 20px; | |
| } | |
| .page-size-container label { | |
| font-weight: 500; | |
| color: #495057; | |
| } | |
| .page-size-container select { | |
| padding: 8px 15px; | |
| border: 2px solid #e9ecef; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| background: white; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .page-size-container select:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .date-filter-container { | |
| display: flex; | |
| align-items: center; | |
| gap: 15px; | |
| margin-top: 20px; | |
| flex-wrap: wrap; | |
| } | |
| .date-input-group { | |
| display: flex; | |
| align-items: center; | |
| gap: 8px; | |
| } | |
| .date-input-group label { | |
| font-weight: 500; | |
| color: #495057; | |
| min-width: 60px; | |
| } | |
| .date-input-group input[type="date"] { | |
| padding: 10px 15px; | |
| border: 2px solid #e9ecef; | |
| border-radius: 8px; | |
| font-size: 14px; | |
| background: white; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| } | |
| .date-input-group input[type="date"]:focus { | |
| outline: none; | |
| border-color: #667eea; | |
| box-shadow: 0 0 0 3px rgba(102, 126, 234, 0.1); | |
| } | |
| .date-shortcuts { | |
| display: flex; | |
| gap: 8px; | |
| flex-wrap: wrap; | |
| } | |
| .date-shortcut-btn { | |
| padding: 8px 15px; | |
| border: 1px solid #e9ecef; | |
| background: white; | |
| border-radius: 8px; | |
| cursor: pointer; | |
| font-size: 13px; | |
| color: #495057; | |
| transition: all 0.3s ease; | |
| } | |
| .date-shortcut-btn:hover { | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| } | |
| .date-shortcut-btn.active { | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| } | |
| .main { | |
| padding: 30px; | |
| } | |
| .log-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| margin-bottom: 30px; | |
| background: white; | |
| border-radius: 10px; | |
| overflow: hidden; | |
| box-shadow: 0 2px 10px rgba(0,0,0,0.1); | |
| } | |
| .log-table th { | |
| background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); | |
| color: white; | |
| padding: 15px; | |
| text-align: left; | |
| font-weight: 500; | |
| } | |
| .log-table td { | |
| padding: 15px; | |
| border-bottom: 1px solid #e9ecef; | |
| vertical-align: middle; | |
| } | |
| .log-table tr:hover { | |
| background: #f8f9fa; | |
| } | |
| .log-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .username { | |
| font-weight: 600; | |
| color: #495057; | |
| } | |
| .token { | |
| width: 200px; | |
| font-family: 'Courier New', monospace; | |
| background: #f8f9fa; | |
| padding: 8px 12px; | |
| border-radius: 4px; | |
| font-size: 0.85rem; | |
| color: #6c757d; | |
| word-break: break-all; | |
| cursor: pointer; | |
| transition: all 0.3s ease; | |
| position: relative; | |
| border: 1px solid #e9ecef; | |
| } | |
| .token:hover { | |
| background: #e9ecef; | |
| border-color: #667eea; | |
| } | |
| .token::after { | |
| content: "点击复制"; | |
| position: absolute; | |
| top: -30px; | |
| left: 50%; | |
| transform: translateX(-50%); | |
| background: #333; | |
| color: white; | |
| padding: 4px 8px; | |
| border-radius: 4px; | |
| font-size: 12px; | |
| opacity: 0; | |
| pointer-events: none; | |
| transition: opacity 0.3s ease; | |
| white-space: nowrap; | |
| } | |
| .token:hover::after { | |
| opacity: 1; | |
| } | |
| .copy-success { | |
| background: #d4edda ; | |
| border-color: #28a745 ; | |
| color: #155724 ; | |
| } | |
| .content { | |
| width: 300px; | |
| word-wrap: break-word; | |
| line-height: 1.4; | |
| } | |
| .timestamp { | |
| color: #6c757d; | |
| font-size: 0.9rem; | |
| } | |
| .pagination { | |
| display: flex; | |
| justify-content: center; | |
| align-items: center; | |
| gap: 20px; | |
| margin-top: 30px; | |
| flex-wrap: wrap; | |
| } | |
| .pagination .page-size-container { | |
| margin: 0; | |
| } | |
| .pagination-controls { | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| } | |
| .pagination button { | |
| padding: 8px 12px; | |
| border: 1px solid #dee2e6; | |
| background: white; | |
| cursor: pointer; | |
| border-radius: 5px; | |
| transition: all 0.3s ease; | |
| } | |
| .pagination button:hover:not(:disabled) { | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| } | |
| .pagination button:disabled { | |
| opacity: 0.5; | |
| cursor: not-allowed; | |
| } | |
| .pagination .active { | |
| background: #667eea; | |
| color: white; | |
| border-color: #667eea; | |
| } | |
| .page-info { | |
| color: #6c757d; | |
| margin: 0 15px; | |
| } | |
| .no-data { | |
| text-align: center; | |
| padding: 60px 20px; | |
| color: #6c757d; | |
| } | |
| .no-data i { | |
| font-size: 4rem; | |
| margin-bottom: 20px; | |
| opacity: 0.5; | |
| } | |
| @media (max-width: 768px) { | |
| .container { | |
| margin: 10px; | |
| border-radius: 10px; | |
| } | |
| .header { | |
| padding: 20px; | |
| } | |
| .header h1 { | |
| font-size: 2rem; | |
| } | |
| .controls { | |
| padding: 20px; | |
| } | |
| .search-container { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .search-input { | |
| min-width: auto; | |
| } | |
| .date-filter-container { | |
| flex-direction: column; | |
| align-items: stretch; | |
| } | |
| .date-input-group { | |
| flex-direction: column; | |
| } | |
| .date-input-group label { | |
| min-width: auto; | |
| } | |
| .stats { | |
| justify-content: center; | |
| } | |
| .content { | |
| padding: 20px; | |
| } | |
| .log-table { | |
| font-size: 0.9rem; | |
| } | |
| .log-table th, | |
| .log-table td { | |
| padding: 10px 8px; | |
| } | |
| .error-content { | |
| max-width: 200px; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <div class="header"> | |
| <h1>日志管理系统</h1> | |
| </div> | |
| <div class="controls"> | |
| <div class="search-container"> | |
| <input type="text" id="searchInput" class="search-input" placeholder="搜索用户名..."> | |
| <button class="btn btn-primary" onclick="searchLogs()">搜索</button> | |
| <button class="btn btn-secondary" onclick="refreshLogs()">刷新</button> | |
| </div> | |
| <div class="date-filter-container"> | |
| <div class="date-input-group"> | |
| <label for="fromDate">开始日期:</label> | |
| <input type="date" id="fromDate"> | |
| </div> | |
| <div class="date-input-group"> | |
| <label for="toDate">结束日期:</label> | |
| <input type="date" id="toDate"> | |
| </div> | |
| <button class="btn btn-primary" onclick="filterByDate()">过滤</button> | |
| <div class="date-shortcuts"> | |
| <button class="date-shortcut-btn" onclick="setDateRange('today')">今天</button> | |
| <button class="date-shortcut-btn" onclick="setDateRange('week')">最近7天</button> | |
| <button class="date-shortcut-btn" onclick="setDateRange('month')">最近30天</button> | |
| <button class="date-shortcut-btn" onclick="setDateRange('all')">全部</button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="main"> | |
| <table class="log-table" id="logTable"> | |
| <thead> | |
| <tr> | |
| <th>Type</th> | |
| <th>Source</th> | |
| <th>UID</th> | |
| <th>Username</th> | |
| <th>Token</th> | |
| <th>Content</th> | |
| <th>Timestamp</th> | |
| </tr> | |
| </thead> | |
| <tbody id="logTableBody"> | |
| </tbody> | |
| </table> | |
| <div class="no-data" id="noData" style="display: none;"> | |
| <div>📋</div> | |
| <h3>暂无数据</h3> | |
| <p>没有找到匹配的日志</p> | |
| </div> | |
| <div class="pagination" id="pagination"> | |
| <div class="page-size-container"> | |
| <label for="pageSize">每页显示:</label> | |
| <select id="pageSize" onchange="changePageSize()"> | |
| <option value="20" selected>20条</option> | |
| <option value="40">40条</option> | |
| <option value="60">60条</option> | |
| </select> | |
| </div> | |
| <div class="pagination-controls"> | |
| <button onclick="goToPage(1)" id="firstBtn">首页</button> | |
| <button onclick="goToPage(currentPage - 1)" id="prevBtn">上一页</button> | |
| <span class="page-info" id="pageInfo">第 1 页,共 1 页</span> | |
| <button onclick="goToPage(currentPage + 1)" id="nextBtn">下一页</button> | |
| <button onclick="goToPage(totalPages)" id="lastBtn">末页</button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const data = {{ data | tojson}}; | |
| // 分页配置 | |
| let currentPage = 1; | |
| let pageSize = 20; | |
| let totalPages = 1; | |
| let filteredLogs = [...data]; | |
| let searchTerm = ''; | |
| let currentDateFilter = null; // 记录当前的日期过滤范围 | |
| // 初始化页面 | |
| function initPage() { | |
| // 从URL参数中读取日期范围 | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const fromDate = urlParams.get('from_date'); | |
| const toDate = urlParams.get('to_date'); | |
| if (fromDate) { | |
| document.getElementById('fromDate').value = fromDate; | |
| } | |
| if (toDate) { | |
| document.getElementById('toDate').value = toDate; | |
| } | |
| renderLogs(); | |
| updatePagination(); | |
| } | |
| // 获取今天的日期(YYYY-MM-DD格式) | |
| function getTodayDate() { | |
| const today = new Date(); | |
| return today.toISOString().split('T')[0]; | |
| } | |
| // 获取指定天数前的日期 | |
| function getDateBefore(days) { | |
| const date = new Date(); | |
| date.setDate(date.getDate() - days); | |
| return date.toISOString().split('T')[0]; | |
| } | |
| // 设置日期范围快捷选项 | |
| function setDateRange(range) { | |
| const today = getTodayDate(); | |
| switch(range) { | |
| case 'today': | |
| document.getElementById('fromDate').value = today; | |
| document.getElementById('toDate').value = today; | |
| break; | |
| case 'week': | |
| document.getElementById('fromDate').value = getDateBefore(6); | |
| document.getElementById('toDate').value = today; | |
| break; | |
| case 'month': | |
| document.getElementById('fromDate').value = getDateBefore(29); | |
| document.getElementById('toDate').value = today; | |
| break; | |
| case 'all': | |
| document.getElementById('fromDate').value = ''; | |
| document.getElementById('toDate').value = ''; | |
| break; | |
| } | |
| // 更新快捷按钮的active状态 | |
| updateShortcutButtons(range); | |
| } | |
| // 更新快捷按钮的样式 | |
| function updateShortcutButtons(active) { | |
| const buttons = document.querySelectorAll('.date-shortcut-btn'); | |
| buttons.forEach((btn, index) => { | |
| const ranges = ['today', 'week', 'month', 'all']; | |
| btn.classList.remove('active'); | |
| if (ranges[index] === active) { | |
| btn.classList.add('active'); | |
| } | |
| }); | |
| } | |
| // 按日期过滤 | |
| function filterByDate() { | |
| const fromDate = document.getElementById('fromDate').value; | |
| const toDate = document.getElementById('toDate').value; | |
| if (!fromDate && !toDate) { | |
| // 如果两个都为空,加载所有数据 | |
| currentDateFilter = null; | |
| refreshFromServer(null, null); | |
| return; | |
| } | |
| if (fromDate && toDate) { | |
| if (fromDate > toDate) { | |
| alert('开始日期不能晚于结束日期'); | |
| return; | |
| } | |
| } | |
| // 记录当前的日期过滤 | |
| currentDateFilter = { from_date: fromDate, to_date: toDate }; | |
| // 刷新并带上日期参数 | |
| refreshFromServer(fromDate, toDate); | |
| } | |
| // 从服务器刷新数据,支持日期参数 | |
| function refreshFromServer(fromDate, toDate) { | |
| let url = window.location.pathname; | |
| const params = new URLSearchParams(); | |
| if (fromDate) { | |
| params.append('from_date', fromDate); | |
| } | |
| if (toDate) { | |
| params.append('to_date', toDate); | |
| } | |
| if (params.toString()) { | |
| url += '?' + params.toString(); | |
| } | |
| // 重新加载页面 | |
| window.location.href = url; | |
| } | |
| // 改变每页显示数量 | |
| function changePageSize() { | |
| const select = document.getElementById('pageSize'); | |
| pageSize = parseInt(select.value); | |
| currentPage = 1; // 重置到第一页 | |
| renderLogs(); | |
| updatePagination(); | |
| } | |
| // 渲染日志表格 | |
| function renderLogs() { | |
| const tbody = document.getElementById('logTableBody'); | |
| const noData = document.getElementById('noData'); | |
| const table = document.getElementById('logTable'); | |
| if (filteredLogs.length === 0) { | |
| table.style.display = 'none'; | |
| noData.style.display = 'block'; | |
| return; | |
| } | |
| table.style.display = 'table'; | |
| noData.style.display = 'none'; | |
| totalPages = Math.ceil(filteredLogs.length / pageSize); | |
| const startIndex = (currentPage - 1) * pageSize; | |
| const endIndex = startIndex + pageSize; | |
| const currentLogs = filteredLogs.slice(startIndex, endIndex); | |
| tbody.innerHTML = ''; | |
| currentLogs.forEach(log => { | |
| const row = document.createElement('tr'); | |
| row.innerHTML = ` | |
| <td> | |
| <div class="type">${log.type}</div> | |
| </td> | |
| <td> | |
| <div class="uid">${log.source}</div> | |
| </td> | |
| <td> | |
| <div class="uid">${log.uid}</div> | |
| </td> | |
| <td> | |
| <div class="username">${log.username}</div> | |
| </td> | |
| <td> | |
| <div class="token" onclick="copyToken('${log.token}')" title="点击复制">${log.token}</div> | |
| </td> | |
| <td> | |
| <div class="content">${log.content}</div> | |
| </td> | |
| <td> | |
| <div class="timestamp">${log.timestamp}</div> | |
| </td> | |
| `; | |
| tbody.appendChild(row); | |
| }); | |
| } | |
| // 复制token到剪贴板 | |
| function copyToken(token) { | |
| navigator.clipboard.writeText(token).then(function() { | |
| // 显示复制成功的视觉反馈 | |
| const tokenElements = document.querySelectorAll('.token'); | |
| tokenElements.forEach(el => { | |
| if (el.textContent === token) { | |
| el.classList.add('copy-success'); | |
| setTimeout(() => { | |
| el.classList.remove('copy-success'); | |
| }, 1000); | |
| } | |
| }); | |
| }).catch(function(err) { | |
| console.error('复制失败: ', err); | |
| // 降级方案:使用旧的复制方法 | |
| const textArea = document.createElement('textarea'); | |
| textArea.value = token; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| }); | |
| } | |
| // 更新分页控件 | |
| function updatePagination() { | |
| const pageInfo = document.getElementById('pageInfo'); | |
| const firstBtn = document.getElementById('firstBtn'); | |
| const prevBtn = document.getElementById('prevBtn'); | |
| const nextBtn = document.getElementById('nextBtn'); | |
| const lastBtn = document.getElementById('lastBtn'); | |
| pageInfo.textContent = `第 ${currentPage} 页,共 ${totalPages} 页`; | |
| firstBtn.disabled = currentPage === 1; | |
| prevBtn.disabled = currentPage === 1; | |
| nextBtn.disabled = currentPage === totalPages; | |
| lastBtn.disabled = currentPage === totalPages; | |
| } | |
| // 跳转到指定页面 | |
| function goToPage(page) { | |
| if (page < 1 || page > totalPages) return; | |
| currentPage = page; | |
| renderLogs(); | |
| updatePagination(); | |
| } | |
| // 搜索日志 | |
| function searchLogs() { | |
| searchTerm = document.getElementById('searchInput').value.trim(); | |
| if (searchTerm === '') { | |
| filteredLogs = [...data]; | |
| } else { | |
| filteredLogs = data.filter(log => | |
| log.username.toLowerCase().includes(searchTerm.toLowerCase()) | |
| ); | |
| } | |
| currentPage = 1; | |
| renderLogs(); | |
| updatePagination(); | |
| } | |
| // 刷新日志 | |
| function refreshLogs() { | |
| // 如果有日期过滤,使用带日期参数的刷新 | |
| if (currentDateFilter) { | |
| refreshFromServer(currentDateFilter.from_date, currentDateFilter.to_date); | |
| } else { | |
| // 获取当前的URL参数 | |
| const urlParams = new URLSearchParams(window.location.search); | |
| const fromDate = urlParams.get('from_date'); | |
| const toDate = urlParams.get('to_date'); | |
| if (fromDate || toDate) { | |
| refreshFromServer(fromDate, toDate); | |
| } else { | |
| // 模拟刷新数据 | |
| console.log('刷新日志数据...'); | |
| currentPage = 1; | |
| renderLogs(); | |
| updatePagination(); | |
| } | |
| } | |
| } | |
| // 监听回车键搜索 | |
| document.getElementById('searchInput').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| searchLogs(); | |
| } | |
| }); | |
| // 监听日期输入框的回车键 | |
| document.getElementById('fromDate').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| filterByDate(); | |
| } | |
| }); | |
| document.getElementById('toDate').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') { | |
| filterByDate(); | |
| } | |
| }); | |
| // 页面加载完成后初始化 | |
| document.addEventListener('DOMContentLoaded', initPage); | |
| </script> | |
| </body> | |
| </html> | |