/** * 班级操行分管理系统 - 公共JS * * 开发者: Canglan * 联系方式: admin@sea-studio.top * 版权归属: Sea Network Technology Studio * 许可证: MIT License * * 版权所有 © Sea Network Technology Studio */ function getToken() { return localStorage.getItem(window.JWT_STORAGE_KEY || 'class_system_token'); } function getUserInfo() { const userStr = localStorage.getItem(window.USER_STORAGE_KEY || 'class_system_user'); if (!userStr) return null; try { return JSON.parse(userStr); } catch { return null; } } function setUserInfo(user) { localStorage.setItem(window.USER_STORAGE_KEY || 'class_system_user', JSON.stringify(user)); } function clearAuth() { localStorage.removeItem(window.JWT_STORAGE_KEY || 'class_system_token'); localStorage.removeItem(window.USER_STORAGE_KEY || 'class_system_user'); } async function apiRequest(url, options = {}) { const token = getToken(); const headers = { 'Content-Type': 'application/json', ...options.headers }; if (token) { headers['Authorization'] = `Bearer ${token}`; } const baseUrl = window.API_BASE_URL; const fullUrl = `${baseUrl}${url}`; try { const response = await fetch(fullUrl, { ...options, headers }); const data = await response.json(); if (response.status === 401) { clearAuth(); // 同步清除 PHP Session,防止 index.php 302 重定向循环 try { await fetch('/api/clear_session.php', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); } catch (e) { console.warn('[Auth] 清除PHP Session失败:', e); } // 防循环机制:检查是否已在登录页 if (window.location.pathname === '/index.php' || window.location.pathname === '/') { console.warn('[Auth] 已在登录页收到401,停止重定向'); return null; } // 防循环机制:5秒内重复401则停止重定向 const now = Date.now(); const lastRedirect = parseInt(sessionStorage.getItem('_last_401_redirect') || '0'); if (now - lastRedirect < 5000) { console.warn('[Auth] 5秒内重复401,停止重定向。请检查Token是否有效。'); return null; } sessionStorage.setItem('_last_401_redirect', now.toString()); window.location.href = '/index.php'; return null; } return data; } catch (error) { console.error('API请求错误:', error); showToast('网络错误,请稍后重试', 'error'); return null; } } function apiGet(url, params = {}) { const queryString = new URLSearchParams(params).toString(); const fullUrl = queryString ? `${url}?${queryString}` : url; return apiRequest(fullUrl, { method: 'GET' }); } function apiPost(url, data = {}) { return apiRequest(url, { method: 'POST', body: JSON.stringify(data) }); } function apiPut(url, data = {}) { return apiRequest(url, { method: 'PUT', body: JSON.stringify(data) }); } function apiDelete(url) { return apiRequest(url, { method: 'DELETE' }); } function showToast(message, type = 'success') { const toast = document.createElement('div'); toast.className = `toast toast-${type}`; toast.textContent = message; document.body.appendChild(toast); setTimeout(() => toast.remove(), 3000); } function formatDate(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`; } function formatDateTime(dateStr) { if (!dateStr) return '-'; const date = new Date(dateStr); return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`; } function getStatusBadge(status, type = 'attendance') { const statusMap = { attendance: { 'present': '出勤', 'absent': '缺勤', 'late': '迟到', 'leave': '请假' } }; const texts = statusMap[type] || statusMap.attendance; const text = texts[status] || status; let className = 'status-badge '; switch (status) { case 'present': className += 'status-submitted'; break; case 'absent': className += 'status-not_submitted'; break; case 'late': className += 'status-late'; break; case 'leave': className += 'status-leave'; break; default: className += 'status-not_submitted'; } return `${text}`; } async function logout() { // 清除 PHP Session try { await fetch('/api/clear_session.php', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); } catch (e) { console.warn('清除Session失败', e); } // 清除后端 Token try { await apiPost('/api/auth/logout'); } catch (e) { console.warn('后端登出失败', e); } // 清除 localStorage clearAuth(); // 跳转回登录页 window.location.href = '/index.php'; } function escapeHtml(str) { if (!str) return ''; return String(str) .replace(/&/g, '\x26amp;') .replace(//g, '\x26gt;') .replace(/"/g, '\x26quot;') .replace(/'/g, '\x26#x27;'); } /** * 智能分页渲染(最多显示7个页码 + 跳转输入框) * @param {string|HTMLElement} container - 分页容器ID或DOM元素 * @param {number} currentPage - 当前页码 * @param {number} totalPages - 总页数 * @param {function} onPageChange - 页码变化回调函数,参数为新的页码 */ function renderSmartPagination(container, currentPage, totalPages, onPageChange) { if (typeof container === 'string') { container = document.getElementById(container); } if (!container || totalPages <= 1) { if (container) container.innerHTML = ''; return; } const MAX_VISIBLE = 7; let html = ''; // 上一页按钮 if (currentPage > 1) { html += `« 上一页`; } if (totalPages <= MAX_VISIBLE) { // 总页数不超过最大显示数,全部显示 for (let i = 1; i <= totalPages; i++) { if (i === currentPage) { html += `${i}`; } else { html += `${i}`; } } } else { // 需要省略号 // 始终显示第1页 if (currentPage === 1) { html += `1`; } else { html += `1`; } // 计算中间页码范围 let start = Math.max(2, currentPage - 2); let end = Math.min(totalPages - 1, currentPage + 2); // 调整确保中间至少有3个页码(加上首尾共5-7个) if (currentPage <= 3) { end = Math.min(5, totalPages - 1); } if (currentPage >= totalPages - 2) { start = Math.max(2, totalPages - 4); } // 前省略号 if (start > 2) { html += `...`; } // 中间页码 for (let i = start; i <= end; i++) { if (i === currentPage) { html += `${i}`; } else { html += `${i}`; } } // 后省略号 if (end < totalPages - 1) { html += `...`; } // 始终显示最后一页 if (currentPage === totalPages) { html += `${totalPages}`; } else { html += `${totalPages}`; } } // 下一页按钮 if (currentPage < totalPages) { html += `下一页 »`; } // 页码跳转 html += `跳至 / ${totalPages}页`; container.innerHTML = html; // 绑定页码点击事件 container.querySelectorAll('a[data-page]').forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const page = parseInt(this.dataset.page); if (page && page !== currentPage && page >= 1 && page <= totalPages) { onPageChange(page); } }); }); // 绑定跳转输入框事件 const jumpInput = container.querySelector('.page-jump input'); if (jumpInput) { jumpInput.addEventListener('keydown', function(e) { if (e.key === 'Enter') { e.preventDefault(); const page = parseInt(this.value); if (page && page >= 1 && page <= totalPages) { onPageChange(page); } else { showToast(`请输入1-${totalPages}之间的页码`, 'warning'); } } }); } } document.addEventListener('DOMContentLoaded', () => { const user = getUserInfo(); const userNameSpan = document.getElementById('userName'); if (userNameSpan && user) { userNameSpan.textContent = user.real_name || user.username; } const logoutBtn = document.getElementById('logoutBtn'); if (logoutBtn) { logoutBtn.addEventListener('click', logout); } }); function toggleActionDropdown(el) { var dropdown = el.closest('.action-dropdown'); if (!dropdown) return; var menu = dropdown.querySelector('.action-dropdown-menu'); if (!menu) return; var isOpen = menu.classList.contains('show'); // 先关闭所有 document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) { m.classList.remove('show'); var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle'); if (toggle) toggle.classList.remove('open'); }); if (!isOpen) { menu.classList.add('show'); el.classList.add('open'); } } document.addEventListener('click', function(e) { if (!e.target.closest('.action-dropdown')) { document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) { m.classList.remove('show'); var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle'); if (toggle) toggle.classList.remove('open'); }); } }); // 全局textarea键盘事件:Enter提交表单,Ctrl+Enter换行 document.addEventListener('keydown', function(e) { if (e.target.tagName !== 'TEXTAREA') return; if (e.key === 'Enter' && !e.ctrlKey && !e.shiftKey && !e.metaKey) { // Enter键提交表单 e.preventDefault(); var form = e.target.closest('form'); if (form) { // 触发form的submit事件 var submitEvent = new Event('submit', { cancelable: true, bubbles: true }); form.dispatchEvent(submitEvent); } } // Ctrl+Enter和Shift+Enter保持默认换行行为(不拦截) }); window.selectDeductionType = function(points, reason) { var pointsEl = document.getElementById('pointsChange'); var reasonEl = document.getElementById('pointsReason'); if (points === 0 && reason === '') { // 自定义模式 - 清空分值和原因,聚焦原因输入框 if (pointsEl) pointsEl.value = ''; if (reasonEl) { reasonEl.value = ''; reasonEl.focus(); } } else if (points === null || points === undefined) { // 类别模式 - 仅填充原因,聚焦分值输入框 if (reasonEl) reasonEl.value = reason; if (pointsEl) { pointsEl.value = ''; pointsEl.focus(); } } else { // 预设模式 - 同时填充分值和原因 if (pointsEl) pointsEl.value = points; if (reasonEl) reasonEl.value = reason; } };