354 lines
11 KiB
JavaScript
354 lines
11 KiB
JavaScript
/**
|
||
* 班级操行分管理系统 - 公共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 = 'homework') {
|
||
const statusMap = {
|
||
homework: {
|
||
'submitted': '已提交',
|
||
'not_submitted': '未提交',
|
||
'late': '迟交'
|
||
},
|
||
attendance: {
|
||
'present': '出勤',
|
||
'absent': '缺勤',
|
||
'late': '迟到',
|
||
'leave': '请假'
|
||
}
|
||
};
|
||
const texts = statusMap[type] || statusMap.homework;
|
||
const text = texts[status] || status;
|
||
let className = 'status-badge ';
|
||
switch (status) {
|
||
case 'submitted':
|
||
case 'present':
|
||
className += 'status-submitted';
|
||
break;
|
||
case 'not_submitted':
|
||
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 `<span class="${className}">${text}</span>`;
|
||
}
|
||
|
||
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 str.replace(/[&<>]/g, function(m) {
|
||
if (m === '&') return '&';
|
||
if (m === '<') return '<';
|
||
if (m === '>') return '>';
|
||
return m;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 智能分页渲染(最多显示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 += `<a href="#" class="page-nav" data-page="${currentPage - 1}">« 上一页</a>`;
|
||
}
|
||
|
||
if (totalPages <= MAX_VISIBLE) {
|
||
// 总页数不超过最大显示数,全部显示
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
if (i === currentPage) {
|
||
html += `<span class="active">${i}</span>`;
|
||
} else {
|
||
html += `<a href="#" data-page="${i}">${i}</a>`;
|
||
}
|
||
}
|
||
} else {
|
||
// 需要省略号
|
||
// 始终显示第1页
|
||
if (currentPage === 1) {
|
||
html += `<span class="active">1</span>`;
|
||
} else {
|
||
html += `<a href="#" data-page="1">1</a>`;
|
||
}
|
||
|
||
// 计算中间页码范围
|
||
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 += `<span class="ellipsis">...</span>`;
|
||
}
|
||
|
||
// 中间页码
|
||
for (let i = start; i <= end; i++) {
|
||
if (i === currentPage) {
|
||
html += `<span class="active">${i}</span>`;
|
||
} else {
|
||
html += `<a href="#" data-page="${i}">${i}</a>`;
|
||
}
|
||
}
|
||
|
||
// 后省略号
|
||
if (end < totalPages - 1) {
|
||
html += `<span class="ellipsis">...</span>`;
|
||
}
|
||
|
||
// 始终显示最后一页
|
||
if (currentPage === totalPages) {
|
||
html += `<span class="active">${totalPages}</span>`;
|
||
} else {
|
||
html += `<a href="#" data-page="${totalPages}">${totalPages}</a>`;
|
||
}
|
||
}
|
||
|
||
// 下一页按钮
|
||
if (currentPage < totalPages) {
|
||
html += `<a href="#" class="page-nav" data-page="${currentPage + 1}">下一页 »</a>`;
|
||
}
|
||
|
||
// 页码跳转
|
||
html += `<span class="page-jump">跳至 <input type="number" min="1" max="${totalPages}" placeholder="页码"> / ${totalPages}页</span>`;
|
||
|
||
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);
|
||
}
|
||
});
|
||
|
||
// 全局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保持默认换行行为(不拦截)
|
||
}); |