Files
2026-05-29 21:50:06 +08:00

425 lines
13 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 班级操行分管理系统 - 公共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 `<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 String(str)
.replace(/&/g, '\x26amp;')
.replace(/</g, '\x26lt;')
.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 += `<a href="#" class="page-nav" data-page="${currentPage - 1}">&laquo; 上一页</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}">下一页 &raquo;</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);
}
});
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');
// 先关闭所有
closeAllDropdowns();
if (!isOpen) {
// 使用 fixed 定位,避免被 overflow 容器裁剪
var rect = el.getBoundingClientRect();
// 先显示以便测量高度
menu.classList.add('show');
menu.style.position = 'fixed';
menu.style.bottom = 'auto';
menu.style.right = 'auto';
menu.style.transform = 'none';
var menuHeight = menu.offsetHeight;
// 智能判断:按钮在上半部分则菜单显示在下方,否则显示在上方
if (rect.top < window.innerHeight / 2) {
menu.style.top = rect.bottom + 'px';
} else {
menu.style.top = (rect.top - menuHeight) + 'px';
}
menu.style.left = Math.min(rect.left, window.innerWidth - 130) + 'px';
el.classList.add('open');
}
}
function closeAllDropdowns() {
document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) {
m.classList.remove('show');
m.style.position = '';
m.style.left = '';
m.style.top = '';
m.style.bottom = '';
m.style.right = '';
m.style.transform = '';
var toggle = m.closest('.action-dropdown');
if (toggle) {
var btn = toggle.querySelector('.action-dropdown-toggle');
if (btn) btn.classList.remove('open');
}
});
}
document.addEventListener('click', function(e) {
if (!e.target.closest('.action-dropdown')) {
closeAllDropdowns();
}
});
// 全局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;
}
};