| <!doctype html> |
| <html lang="es"> |
| <head> |
| <meta charset="utf-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no"> |
| <meta name="description" content="Gestión de Usuarios"> |
| <meta name="author" content="Sistema Médico"> |
| <link rel="icon" href="favicon.ico"> |
| <title>Usuarios</title> |
|
|
| <link rel="stylesheet" href="css/simplebar.css"> |
| <link href="https://fonts.googleapis.com/css2?family=Overpass:ital,wght@0,100;0,200;0,300;0,400;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet"> |
| <script src="https://cdn.jsdelivr.net/npm/feather-icons/dist/feather.min.js"></script> |
| <script> |
| (function() { |
| var dark = localStorage.getItem('theme') === 'dark'; |
| document.write( |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'dark' : 'light') + '.css" id="' + (dark ? 'dark' : 'light') + 'Theme">' + |
| '<link rel="stylesheet" href="css/app-' + (dark ? 'light' : 'dark') + '.css" id="' + (dark ? 'light' : 'dark') + 'Theme" disabled>' |
| ); |
| })(); |
| </script> |
| <style> |
| |
| #toast-container { |
| position: fixed; |
| bottom: 1.5rem; |
| right: 1.5rem; |
| z-index: 99999; |
| display: flex; |
| flex-direction: column; |
| gap: 0.6rem; |
| pointer-events: none; |
| } |
| .toast-notify { |
| display: flex; |
| align-items: flex-start; |
| gap: 12px; |
| min-width: 300px; |
| max-width: 380px; |
| padding: 14px 16px; |
| border-radius: 10px; |
| box-shadow: 0 8px 24px rgba(0,0,0,0.15); |
| background: #fff; |
| border-left: 5px solid #ccc; |
| pointer-events: all; |
| opacity: 0; |
| transform: translateX(60px); |
| transition: opacity 0.3s ease, transform 0.3s ease; |
| position: relative; |
| overflow: hidden; |
| } |
| .toast-notify.show { opacity: 1; transform: translateX(0); } |
| .toast-notify.hide { opacity: 0; transform: translateX(60px); } |
| .toast-notify.toast-success { border-left-color: #28a745; } |
| .toast-notify.toast-error { border-left-color: #dc3545; } |
| .toast-notify.toast-warning { border-left-color: #ffc107; } |
| .toast-notify.toast-info { border-left-color: #17a2b8; } |
| .toast-icon { flex-shrink: 0; margin-top: 1px; } |
| .toast-success .toast-icon svg { stroke: #28a745; } |
| .toast-error .toast-icon svg { stroke: #dc3545; } |
| .toast-warning .toast-icon svg { stroke: #ffc107; } |
| .toast-info .toast-icon svg { stroke: #17a2b8; } |
| .toast-body { flex: 1; } |
| .toast-title { font-weight: 600; font-size: 0.875rem; margin-bottom: 2px; } |
| .toast-success .toast-title { color: #155724; } |
| .toast-error .toast-title { color: #721c24; } |
| .toast-warning .toast-title { color: #856404; } |
| .toast-info .toast-title { color: #0c5460; } |
| .toast-msg { font-size: 0.825rem; color: #555; line-height: 1.4; } |
| .toast-close { |
| background: none; border: none; font-size: 1rem; |
| line-height: 1; color: #aaa; cursor: pointer; padding: 0; flex-shrink: 0; |
| } |
| .toast-close:hover { color: #333; } |
| .toast-progress { |
| position: absolute; |
| bottom: 0; left: 0; |
| height: 3px; |
| border-radius: 0 0 0 10px; |
| } |
| .toast-success .toast-progress { background: #28a745; } |
| .toast-error .toast-progress { background: #dc3545; } |
| .toast-warning .toast-progress { background: #ffc107; } |
| .toast-info .toast-progress { background: #17a2b8; } |
| </style> |
| </head> |
|
|
| <body class="vertical light"> |
| <div class="wrapper"> |
| |
| <nav class="topnav navbar navbar-light"> |
| <button type="button" class="navbar-toggler text-muted mt-2 p-0 mr-3 collapseSidebar"> |
| <i data-feather="menu" class="navbar-toggler-icon"></i> |
| </button> |
|
|
| <ul class="nav"> |
| <li class="nav-item"> |
| <a class="nav-link text-muted my-2" href="#" id="modeSwitcher" data-mode="light"> |
| <i data-feather="sun"></i> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a href="#" onclick="confirmLogout()" class="nav-link text-muted" title="Cerrar sesión" style="display: flex; align-items: center; gap: 6px;"> |
| <span class="avatar avatar-sm mt-2"> |
| <img src="./assets/images/persona.png" alt="..." class="avatar-img rounded-circle"> |
| </span> |
| <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" title="Cerrar sesión"> |
| <path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"/> |
| <polyline points="16 17 21 12 16 7"/> |
| <line x1="21" y1="12" x2="9" y2="12"/> |
| </svg> |
| </a> |
| </li> |
| </ul> |
| </nav> |
|
|
| |
| <aside class="sidebar-left border-right bg-white shadow" id="leftSidebar" data-simplebar> |
| <a href="#" class="btn collapseSidebar toggle-btn d-lg-none text-muted ml-2 mt-3" data-toggle="toggle"> |
| <i data-feather="x"><span class="sr-only"></span></i> |
| </a> |
| <nav class="vertnav navbar navbar-light"> |
| |
| <a class="navbar-brand mx-auto mt-2 flex-fill text-center"> |
| <img src="assets/images/RetinAILogo.png" alt="Logo" id="sidebarLogo" style="max-width:140px; transition: opacity 0.3s ease;"> |
| <img src="assets/images/LogoMini.png" alt="Logo Mini" id="sidebarLogoMini" style="max-width:44px; display:none; transition: opacity 0.3s ease;"> |
| </a> |
|
|
| <ul class="navbar-nav flex-fill w-100 mb-2"> |
| <li class="nav-item"> |
| <a class="nav-link" href="./index.html"> |
| <i data-feather="home" class="fe-16"></i> |
| <span class="ml-3 item-text">Dashboard</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./diagnosis.html"> |
| <i data-feather="eye" class="fe-16"></i> |
| <span class="ml-3 item-text">Diagnóstico</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./patients.html"> |
| <i data-feather="users" class="fe-16"></i> |
| <span class="ml-3 item-text">Pacientes</span> |
| </a> |
| </li> |
| <li class="nav-item"> |
| <a class="nav-link" href="./historial-consultas.html"> |
| <i data-feather="file-text" class="fe-16"></i> |
| <span class="ml-3 item-text">Consultas</span> |
| </a> |
| </li> |
| <li class="nav-item active"> |
| <a class="nav-link" href="./users.html"> |
| <i data-feather="user" class="fe-16"></i> |
| <span class="ml-3 item-text">Usuarios</span> |
| </a> |
| </li> |
| </ul> |
| </nav> |
| </aside> |
|
|
| |
| <main role="main" class="main-content"> |
| <div class="container-fluid"> |
| <div class="row justify-content-center"> |
| <div class="col-12"> |
| |
| <div class="row align-items-center mb-2"> |
| <div class="col"> |
| <h2 class="h5 page-title">Gestión de Usuarios</h2> |
| </div> |
| <div class="col-auto"> |
| <button class="btn btn-primary" onclick="loadUsers()"> |
| <i data-feather="refresh-cw" class="fe-16 mr-2"></i>Actualizar |
| </button> |
| <button class="btn btn-success ml-2" onclick="showAddUserModal()"> |
| <i data-feather="plus" class="fe-16 mr-2"></i>Nuevo Usuario |
| </button> |
| </div> |
| </div> |
|
|
| |
| <div class="card shadow"> |
| <div class="card-header"> |
| <h6 class="card-title mb-0"> |
| <i data-feather="users" class="mr-2"></i>Lista de Usuarios |
| </h6> |
| </div> |
| <div class="card-body"> |
| <div id="loadingSpinner" class="text-center py-5"> |
| <div class="spinner-border text-primary" role="status"> |
| <span class="sr-only">Cargando...</span> |
| </div> |
| <p class="text-muted mt-2">Cargando usuarios...</p> |
| </div> |
|
|
| <div id="usersTableContainer" style="display: none;"> |
| <div class="table-responsive"> |
| <table class="table table-striped table-hover"> |
| <thead class="thead-light"> |
| <tr> |
| <th>ID</th> |
| <th>Nombre de Usuario</th> |
| <th>Rol</th> |
| <th>Fecha de Creación</th> |
| <th class="text-center">Acciones</th> |
| </tr> |
| </thead> |
| <tbody id="usersTableBody"> |
| |
| </tbody> |
| </table> |
| </div> |
| </div> |
|
|
| <div id="emptyState" class="text-center py-5" style="display: none;"> |
| <i data-feather="users" class="fe-48 text-muted mb-3"></i> |
| <h6 class="text-muted">No se encontraron usuarios</h6> |
| <p class="text-muted">No hay usuarios registrados en el sistema.</p> |
| <button class="btn btn-primary" onclick="showAddUserModal()"> |
| <i data-feather="plus" class="mr-1"></i>Crear Primer Usuario |
| </button> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </div> |
| </main> |
| </div> |
|
|
| |
| <div class="modal fade" id="userModal" tabindex="-1" role="dialog" aria-labelledby="userModalLabel" aria-hidden="true"> |
| <div class="modal-dialog" role="document"> |
| <div class="modal-content"> |
| <div class="modal-header"> |
| <h5 class="modal-title" id="userModalLabel"> |
| <i data-feather="user" class="mr-2"></i>Nuevo Usuario |
| </h5> |
| <button type="button" class="close" data-dismiss="modal" aria-label="Close"> |
| <span aria-hidden="true">×</span> |
| </button> |
| </div> |
| <div class="modal-body"> |
| <form id="userForm"> |
| <input type="hidden" id="userId"> |
| <div class="form-group"> |
| <label for="username">Nombre de Usuario</label> |
| <input type="text" class="form-control" id="username" required> |
| </div> |
| <div class="form-group"> |
| <label for="password">Contraseña</label> |
| <input type="password" class="form-control" id="password" required> |
| </div> |
| <div class="form-group"> |
| <label for="confirmPassword">Confirmar Contraseña</label> |
| <input type="password" class="form-control" id="confirmPassword" required> |
| </div> |
| <div class="form-group"> |
| <label for="role">Rol</label> |
| <select class="form-control" id="role" required> |
| <option value="Doctor">Doctor</option> |
| <option value="Admin">Administrador</option> |
| </select> |
| </div> |
| </form> |
| </div> |
| <div class="modal-footer"> |
| <button type="button" class="btn btn-secondary" data-dismiss="modal">Cancelar</button> |
| <button type="button" class="btn btn-primary" id="saveUserBtn" onclick="saveUser()">Guardar</button> |
| </div> |
| </div> |
| </div> |
| </div> |
|
|
| <script src="js/jquery.min.js"></script> |
| <script src="js/popper.min.js"></script> |
| <script src="js/bootstrap.min.js"></script> |
| <script src="js/simplebar.min.js"></script> |
| <script src="js/config.js"></script> |
| <script src="api.js"></script> |
|
|
| <script> |
| $(document).ready(function() { |
| checkAuthentication(); |
| |
| loadUsers(); |
| |
| $('#logoutBtn').on('click', function(e) { |
| e.preventDefault(); |
| if (confirm('¿Estás seguro que deseas cerrar sesión?')) { |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); |
| } |
| }); |
| }); |
| |
| function checkAuthentication() { |
| api.getSession().then(r => { if (!r.success) window.location.href = 'auth-login.html'; }).catch(() => { window.location.href = 'auth-login.html'; }); |
| } |
| |
| function loadUsers() { |
| $('#loadingSpinner').show(); |
| $('#usersTableContainer').hide(); |
| $('#emptyState').hide(); |
| |
| api.getUsers().then(function(response) { |
| $('#loadingSpinner').hide(); |
| |
| if (response.success) { |
| if (response.users.length > 0) { |
| renderUsersTable(response.users); |
| $('#usersTableContainer').show(); |
| } else { |
| $('#emptyState').show(); |
| } |
| } else { |
| console.error('Error cargando usuarios:', response.message); |
| showAlert('error', 'Error cargando usuarios: ' + response.message); |
| $('#emptyState').show(); |
| } |
| }); |
| } |
| |
| function renderUsersTable(users) { |
| const tbody = $('#usersTableBody'); |
| tbody.empty(); |
| |
| users.forEach(user => { |
| const row = ` |
| <tr> |
| <td>${user.userID}</td> |
| <td>${user.username}</td> |
| <td> |
| <span class="badge ${user.role === 'Admin' ? 'badge-primary' : 'badge-secondary'}"> |
| ${user.role} |
| </span> |
| </td> |
| <td>${user.creationDate}</td> |
| <td class="text-center"> |
| <div class="btn-group" role="group"> |
| <button class="btn btn-sm btn-outline-primary" |
| onclick="editUser(${user.userID})" |
| title="Editar"> |
| <i data-feather="edit" class="fe-12"></i> |
| </button> |
| <button class="btn btn-sm btn-outline-danger" |
| onclick="confirmDeleteUser(${user.userID})" |
| title="Eliminar"> |
| <i data-feather="trash-2" class="fe-12"></i> |
| </button> |
| </div> |
| </td> |
| </tr> |
| `; |
| tbody.append(row); |
| }); |
| } |
| |
| function showAddUserModal() { |
| $('#userForm')[0].reset(); |
| $('#userId').val(''); |
| $('#userModalLabel').html('<i data-feather="user-plus" class="mr-2"></i>Nuevo Usuario'); |
| $('#userModal').modal('show'); |
| } |
| |
| function editUser(userId) { |
| api.getUsers().then(function(response) { |
| if (response.success) { |
| const user = response.users.find(u => u.userID == userId); |
| if (user) { |
| $('#userId').val(user.userID); |
| $('#username').val(user.username); |
| $('#role').val(user.role); |
| $('#password').val(''); |
| $('#confirmPassword').val(''); |
| |
| $('#userModalLabel').html('<i data-feather="edit" class="mr-2"></i>Editar Usuario'); |
| $('#userModal').modal('show'); |
| } |
| } |
| }); |
| } |
| |
| function saveUser() { |
| const userId = $('#userId').val(); |
| const username = $('#username').val().trim(); |
| const password = $('#password').val(); |
| const confirmPassword = $('#confirmPassword').val(); |
| const role = $('#role').val(); |
| |
| if (!username) { |
| showAlert('error', 'El nombre de usuario es requerido'); |
| return; |
| } |
| |
| if (!userId && (!password || !confirmPassword)) { |
| showAlert('error', 'La contraseña es requerida para nuevos usuarios'); |
| return; |
| } |
| |
| if (password !== confirmPassword) { |
| showAlert('error', 'Las contraseñas no coinciden'); |
| return; |
| } |
| |
| if (userId) { |
| const userData = { |
| userId: userId, |
| username: username, |
| role: role |
| }; |
| |
| if (password) { |
| userData.password = password; |
| } |
| |
| api.updateUser(userId, userData).then(function(response) { |
| if (response.success) { |
| showAlert('success', 'Usuario actualizado exitosamente'); |
| $('#userModal').modal('hide'); |
| loadUsers(); |
| } else { |
| showAlert('error', response.message || 'Error actualizando usuario'); |
| } |
| }); |
| } else { |
| api.createUser(username, password, role).then(function(response) { |
| if (response.success) { |
| showAlert('success', 'Usuario creado exitosamente'); |
| $('#userModal').modal('hide'); |
| loadUsers(); |
| } else { |
| showAlert('error', response.message || 'Error creando usuario'); |
| } |
| }); |
| } |
| } |
| |
| function confirmDeleteUser(userId) { |
| if (confirm('¿Estás seguro que deseas eliminar este usuario? Esta acción no se puede deshacer.')) { |
| deleteUser(userId); |
| } |
| } |
| |
| function deleteUser(userId) { |
| api.deleteUser(userId).then(function(response) { |
| if (response.success) { |
| showAlert('success', 'Usuario eliminado exitosamente'); |
| loadUsers(); |
| } else { |
| showAlert('error', response.message || 'Error eliminando usuario'); |
| } |
| }); |
| } |
| |
| function showAlert(type, message) { |
| if (!document.getElementById('toast-container')) { |
| const container = document.createElement('div'); |
| container.id = 'toast-container'; |
| document.body.appendChild(container); |
| } |
| const container = document.getElementById('toast-container'); |
| |
| const icons = { |
| success: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/><polyline points="22 4 12 14.01 9 11.01"/></svg>', |
| error: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="15" y1="9" x2="9" y2="15"/><line x1="9" y1="9" x2="15" y2="15"/></svg>', |
| warning: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M10.29 3.86L1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>', |
| info: '<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="8" x2="12" y2="12"/><line x1="12" y1="16" x2="12.01" y2="16"/></svg>' |
| }; |
| const titles = { success: 'Éxito', error: 'Error', warning: 'Advertencia', info: 'Información' }; |
| const duration = type === 'error' ? 6000 : 4000; |
| |
| const toast = document.createElement('div'); |
| toast.className = `toast-notify toast-${type}`; |
| toast.innerHTML = ` |
| <div class="toast-icon">${icons[type] || icons.info}</div> |
| <div class="toast-body"> |
| <div class="toast-title">${titles[type] || 'Aviso'}</div> |
| <div class="toast-msg">${message}</div> |
| </div> |
| <button class="toast-close" onclick="this.closest('.toast-notify').remove()">×</button> |
| <div class="toast-progress" style="width:100%;transition:width ${duration}ms linear;"></div> |
| `; |
| container.appendChild(toast); |
| |
| requestAnimationFrame(() => requestAnimationFrame(() => { |
| toast.classList.add('show'); |
| const bar = toast.querySelector('.toast-progress'); |
| if (bar) requestAnimationFrame(() => { bar.style.width = '0%'; }); |
| })); |
| |
| const timer = setTimeout(() => { |
| toast.classList.add('hide'); |
| toast.addEventListener('transitionend', () => toast.remove(), { once: true }); |
| }, duration); |
| |
| toast.querySelector('.toast-close').addEventListener('click', () => clearTimeout(timer)); |
| } |
| </script> |
|
|
| <script> |
| function confirmLogout() { |
| executeLogout(); |
| } |
| |
| function executeLogout() { |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); |
| } |
| </script> |
|
|
| <script> |
| function checkAdminAccess() { |
| api.getSession().then(r => { if (!r.success) { window.location.href = 'auth-login.html'; } else if (r.user.role !== 'Admin') { alert('Acceso denegado: Solo administradores'); window.location.href = 'index.html'; } }).catch(() => { window.location.href = 'auth-login.html'; }); |
| } |
| |
| function renderUsersTableSecure(users) { |
| const tbody = $('#usersTableBody'); |
| tbody.empty(); |
| |
| api.getSession().then(function(currentUserResponse) { if (!currentUserResponse.success) return; const currentUserId = currentUserResponse.user.userID; |
| |
| users.forEach(user => { |
| const canDelete = user.userID !== currentUserId; |
| |
| const deleteButton = canDelete ? |
| `<button class="btn btn-sm btn-outline-danger" |
| onclick="confirmDeleteUser(${user.userID})" |
| title="Eliminar"> |
| <i data-feather="trash-2" class="fe-12"></i> |
| </button>` : |
| `<button class="btn btn-sm btn-outline-secondary" |
| disabled |
| title="No puedes eliminar tu propia cuenta"> |
| <i data-feather="user-x" class="fe-12"></i> |
| </button>`; |
| |
| const row = ` |
| <tr> |
| <td>${user.userID}</td> |
| <td> |
| ${user.username} |
| ${user.userID === currentUserId ? '<span class="badge badge-info ml-2">Tú</span>' : ''} |
| </td> |
| <td> |
| <span class="badge ${user.role === 'Admin' ? 'badge-primary' : 'badge-secondary'}"> |
| ${user.role} |
| </span> |
| </td> |
| <td>${user.creationDate}</td> |
| <td class="text-center"> |
| <div class="btn-group" role="group"> |
| <button class="btn btn-sm btn-outline-primary" |
| onclick="editUser(${user.userID})" |
| title="Editar"> |
| <i data-feather="edit" class="fe-12"></i> |
| </button> |
| ${deleteButton} |
| </div> |
| </td> |
| </tr> |
| `; |
| tbody.append(row); |
| }); |
| }); |
| } |
| |
| function confirmDeleteUserSecure(userId) { |
| api.getSession().then(function(response) { if (!response.success) { showAlert('error', 'Error de autenticación'); return; } if (response.user.userID === userId) { |
| showAlert('error', 'No puedes eliminar tu propia cuenta'); |
| return; |
| } |
| |
| const confirmation = confirm( |
| 'ELIMINAR USUARIO\n\n' + |
| '¿Estás seguro que deseas eliminar este usuario?\n\n' + |
| '• Esta acción NO se puede deshacer\n' + |
| '• Se perderán todos los datos asociados\n' + |
| '• El usuario no podrá acceder al sistema\n\n' + |
| 'Escribe "CONFIRMAR" para continuar:' |
| ); |
| |
| if (confirmation) { |
| const secondConfirmation = prompt( |
| 'Para confirmar la eliminación, escribe: CONFIRMAR' |
| ); |
| |
| if (secondConfirmation === 'CONFIRMAR') { |
| deleteUser(userId); |
| } else { |
| showAlert('info', 'Eliminación cancelada - Confirmación incorrecta'); |
| } |
| } |
| }); |
| } |
| |
| $(document).ready(function() { |
| checkAdminAccess(); |
| |
| setTimeout(() => { |
| checkAuthentication(); |
| |
| loadUsers(); |
| |
| $('#logoutBtn').on('click', function(e) { |
| e.preventDefault(); |
| if (confirm('¿Estás seguro que deseas cerrar sesión?')) { |
| api.logout().then(() => { window.location.href = 'auth-login.html'; }); |
| } |
| }); |
| }, 500); |
| }); |
| |
| window.renderUsersTable = renderUsersTableSecure; |
| |
| window.confirmDeleteUser = confirmDeleteUserSecure; |
| </script> |
|
|
| <script> |
| (function() { |
| function centerWindow() { |
| try { |
| const screenWidth = window.screen.availWidth; |
| const screenHeight = window.screen.availHeight; |
| const x = Math.floor((screenWidth - 1200) / 2); |
| const y = Math.floor((screenHeight - 800) / 2); |
| |
| if (window.moveTo) { |
| window.moveTo(Math.max(0, x), Math.max(0, y)); |
| } |
| } catch (e) { |
| console.warn('No se pudo centrar ventana:', e); |
| } |
| } |
| |
| if (document.readyState === 'loading') { |
| document.addEventListener('DOMContentLoaded', centerWindow); |
| } else { |
| centerWindow(); |
| } |
| |
| window.addEventListener('load', centerWindow); |
| window.addEventListener('focus', centerWindow); |
| })(); |
| </script> |
| <script> |
| |
| |
| function _initFeather() { |
| if (typeof feather !== 'undefined') feather.replace({ 'stroke-width': 1.8 }); |
| } |
| document.addEventListener('DOMContentLoaded', _initFeather); |
| |
| var _fObserver = new MutationObserver(function(mutations) { |
| var needsRefresh = mutations.some(function(m) { |
| return m.addedNodes.length > 0; |
| }); |
| if (needsRefresh) _initFeather(); |
| }); |
| document.addEventListener('DOMContentLoaded', function() { |
| _fObserver.observe(document.body, { childList: true, subtree: true }); |
| }); |
| </script> |
| <script> |
| (function () { |
| const overlay = document.createElement('div'); |
| overlay.id = 'theme-overlay'; |
| overlay.style.cssText = [ |
| 'position:fixed','inset:0','z-index:99999', |
| 'pointer-events:none','opacity:0', |
| 'transition:opacity 0.18s ease','background:#fff' |
| ].join(';'); |
| document.body.appendChild(overlay); |
| |
| function switchTheme() { |
| const lightTheme = document.getElementById('lightTheme'); |
| const darkTheme = document.getElementById('darkTheme'); |
| const goingDark = !darkTheme.disabled; |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; |
| overlay.style.opacity = '1'; |
| setTimeout(function () { |
| lightTheme.disabled = !lightTheme.disabled; |
| darkTheme.disabled = !darkTheme.disabled; |
| setTimeout(function () { overlay.style.opacity = '0'; }, 50); |
| }, 180); |
| } |
| |
| document.addEventListener('DOMContentLoaded', function () { |
| const btn = document.getElementById('modeSwitcher'); |
| if (!btn) return; |
| const newBtn = btn.cloneNode(true); |
| btn.parentNode.replaceChild(newBtn, btn); |
| newBtn.addEventListener('click', function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| switchTheme(); |
| }); |
| }); |
| })(); |
| </script> |
| <script> |
| (function () { |
| |
| function applyTheme(theme) { |
| const lightTheme = document.getElementById('lightTheme'); |
| const darkTheme = document.getElementById('darkTheme'); |
| if (theme === 'dark') { |
| lightTheme.disabled = true; |
| darkTheme.disabled = false; |
| } else { |
| lightTheme.disabled = false; |
| darkTheme.disabled = true; |
| } |
| } |
| |
| applyTheme(localStorage.getItem('theme') || 'light'); |
| |
| |
| window.addEventListener('storage', function(e) { |
| if (e.key === 'theme') { |
| applyTheme(e.newValue); |
| } |
| }); |
| |
| |
| const overlay = document.createElement('div'); |
| overlay.id = 'theme-overlay'; |
| overlay.style.cssText = [ |
| 'position:fixed','inset:0','z-index:99999', |
| 'pointer-events:none','opacity:0', |
| 'transition:opacity 0.18s ease','background:#fff' |
| ].join(';'); |
| document.body.appendChild(overlay); |
| |
| function switchTheme() { |
| const darkTheme = document.getElementById('darkTheme'); |
| const goingDark = darkTheme.disabled; |
| overlay.style.background = goingDark ? '#fff' : '#1a1a2e'; |
| overlay.style.opacity = '1'; |
| setTimeout(function () { |
| const newTheme = goingDark ? 'dark' : 'light'; |
| applyTheme(newTheme); |
| localStorage.setItem('theme', newTheme); |
| setTimeout(function () { overlay.style.opacity = '0'; }, 50); |
| }, 180); |
| } |
| |
| document.addEventListener('DOMContentLoaded', function () { |
| const btn = document.getElementById('modeSwitcher'); |
| if (!btn) return; |
| const newBtn = btn.cloneNode(true); |
| btn.parentNode.replaceChild(newBtn, btn); |
| newBtn.addEventListener('click', function (e) { |
| e.preventDefault(); |
| e.stopPropagation(); |
| switchTheme(); |
| }); |
| }); |
| })(); |
| |
| (function() { |
| var sidebar = document.getElementById('leftSidebar'); |
| var logo = document.getElementById('sidebarLogo'); |
| var logoMini = document.getElementById('sidebarLogoMini'); |
| if (!sidebar || !logo || !logoMini) return; |
| |
| function updateLogo() { |
| var isCollapsed = sidebar.classList.contains('collapsed') |
| || document.body.classList.contains('collapsed') |
| || sidebar.offsetWidth < 100; |
| |
| logo.style.display = isCollapsed ? 'none' : ''; |
| logoMini.style.display = isCollapsed ? 'block' : 'none'; |
| } |
| |
| var observer = new MutationObserver(updateLogo); |
| observer.observe(sidebar, { attributes: true, attributeFilter: ['class', 'style'] }); |
| observer.observe(document.body, { attributes: true, attributeFilter: ['class'] }); |
| |
| updateLogo(); |
| })(); |
| </script> |
| </body> |
| </html> |