/**
* 班级操行分管理系统 - 公共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;
}
};