/
Cooolprogrammers
/
web-nodejs-labs
Обзор
Документация
Войти
/
Cooolprogrammers
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
public/lab11/task3/index.html
596 строк
21 KB
darkvoid6@mail.ru
ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ
18 дек 2025, 07:59
18 дек 2025, 07:59
f2c4f06
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>ЛР11. Задача 2: Справочники (RBAC)</title> <!-- Иконки FontAwesome --> <script src="https://kit.fontawesome.com/a2d9d5a64b.js" crossorigin="anonymous"></script> <style> :root { --primary: #10b981; --primary-hover: #059669; --success: #22c55e; --danger: #f43f5e; --warning: #f59e0b; --bg-dark: #0f172a; --bg-card: #1e293b; --text-main: #f1f5f9; --text-muted: #94a3b8; --border: #334155; } body { font-family: 'Inter', system-ui, sans-serif; padding: 20px; background-color: var(--bg-dark); color: var(--text-main); margin: 0; line-height: 1.5; } /* Панель авторизации */ .auth-bar { background: var(--bg-card); padding: 15px 20px; border-radius: 8px; box-shadow: 0 2px 10px rgba(0,0,0,0.3); margin-bottom: 20px; display: flex; justify-content: space-between; align-items: center; border: 1px solid var(--border); } .auth-bar input { padding: 8px; background: #0f172a; border: 1px solid var(--border); border-radius: 6px; color: var(--text-main); } .user-info { font-weight: bold; color: var(--primary); } /* Основные элементы */ .controls { margin: 20px 0; display: flex; align-items: center; gap: 10px; } select { padding: 8px; border-radius: 6px; border: 1px solid var(--border); font-size: 14px; background: var(--bg-card); color: var(--text-main); } /* Таблица */ table { width: 100%; border-collapse: collapse; background: var(--bg-card); border-radius: 8px; overflow: hidden; box-shadow: 0 2px 5px rgba(0,0,0,0.2); border: 1px solid var(--border); } th, td { padding: 12px 15px; text-align: left; border-bottom: 1px solid var(--border); } th { background-color: var(--bg-dark); color: var(--primary); font-weight: 600; } tr:hover { background-color: rgba(16, 185, 129, 0.1); } /* Кнопки */ .btn { padding: 8px 12px; border: none; border-radius: 6px; cursor: pointer; color: white; transition: 0.2s; font-weight: 600; background: var(--primary); } .btn:hover { opacity: 0.9; transform: translateY(-1px); } .btn-primary { background: var(--primary); } .btn-success { background: var(--success); } .btn-danger { background: var(--danger); } .btn-warning { background: var(--warning); color: #000; } .btn-sm { padding: 5px 10px; font-size: 12px; } /* Модальное окно / Форма */ .modal-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.7); display: none; justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: var(--bg-card); padding: 25px; border-radius: 12px; width: 400px; box-shadow: 0 20px 25px rgba(0,0,0,0.3); border: 1px solid var(--border); color: var(--text-main); } .form-group { margin-bottom: 15px; } .form-group label { display: block; margin-bottom: 5px; font-weight: 600; color: var(--text-main); } .form-group input, .form-group select { width: 100%; padding: 8px; background: #0f172a; border: 1px solid var(--border); border-radius: 6px; box-sizing: border-box; color: var(--text-main); } /* Утилиты */ .hidden { display: none !important; } h3 { color: var(--primary); margin: 0; } i { color: var(--primary); } </style> </head> <body> <!-- 1. Верхняя панель авторизации --> <div class="auth-bar"> <div> <h3><i class="fas fa-database"></i> Lab 11: Task 2 (Справочники)</h3> </div> <!-- Форма входа (видна, если нет токена) --> <div id="loginForm"> <input type="text" id="loginUsername" placeholder="Логин"> <input type="password" id="loginPassword" placeholder="Пароль"> <button class="btn btn-primary" onclick="handleLogin()">Войти</button> </div> <!-- Информация о пользователе (видна, если есть токен) --> <div id="userInfo" class="hidden"> <span id="userDisplay" class="user-info"></span> <button class="btn btn-warning btn-sm" onclick="handleLogout()">Выйти</button> </div> </div> <!-- 2. Выбор таблицы и управление --> <div class="controls"> <label style="color: var(--text-main);">Справочник:</label> <select id="tableSelector"> <option value="persons">Люди (Persons)</option> <option value="departments">Отделы (Departments)</option> <option value="positions">Должности (Positions)</option> <option value="teachers">Преподаватели</option> <option value="groups">Группы</option> <option value="buildings">Корпуса</option> <option value="classrooms">Аудитории</option> <option value="subjects">Дисциплины</option> <option value="lesson_types">Типы занятий</option> </select> <button class="btn btn-primary" onclick="loadData()"><i class="fas fa-sync"></i> Обновить</button> <!-- Кнопка только для Админа --> <button id="addBtn" class="btn btn-success hidden" onclick="openModal()"><i class="fas fa-plus"></i> Добавить запись</button> </div> <!-- 3. Таблица данных --> <div id="tableContainer"> <p>Загрузка данных...</p> </div> <!-- 4. Модальное окно (Форма создания/редактирования) --> <div id="modal" class="modal-overlay"> <div class="modal-content"> <h3 id="modalTitle" style="color: var(--primary); margin-bottom: 20px;">Запись</h3> <form id="editForm"> <div id="formFields"> <!-- Поля генерируются JS --> </div> <div style="margin-top: 20px; text-align: right; display: flex; gap: 10px; justify-content: flex-end;"> <button type="button" class="btn btn-warning" onclick="closeModal()">Отмена</button> <button type="submit" class="btn btn-success">Сохранить</button> </div> </form> </div> </div> <script> // === КОНФИГУРАЦИЯ API === // Убедитесь, что пути совпадают с вашим backend/routes/index.js const API_LOGIN = '/api/lab11/login'; const API_BASE = '/api/lab11/task2'; // === СОСТОЯНИЕ ПРИЛОЖЕНИЯ === let currentTable = 'persons'; let currentData = []; let editingId = null; // Если null -> создание, если число -> редактирование // Получаем данные из LocalStorage let authToken = localStorage.getItem('lab11_token'); let currentUser = JSON.parse(localStorage.getItem('lab11_user') || 'null'); // === КОНФИГУРАЦИЯ ПОЛЕЙ ТАБЛИЦ === // Описываем, какие поля есть в таблицах и откуда брать данные для выпадающих списков function getTableConfig(tableName) { switch (tableName) { case 'persons': return [ { name: 'last_name', label: 'Фамилия', type: 'text' }, { name: 'first_name', label: 'Имя', type: 'text' }, { name: 'middle_name', label: 'Отчество', type: 'text' } ]; case 'departments': return [ { name: 'name', label: 'Название отдела', type: 'text' } ]; case 'positions': return [ { name: 'title', label: 'Название должности', type: 'text' } ]; case 'buildings': return [ { name: 'name', label: 'Корпус', type: 'text' }, { name: 'address', label: 'Адрес', type: 'text' } ]; case 'lesson_types': return [ { name: 'name', label: 'Тип', type: 'text' } ]; // Связные таблицы case 'groups': return [ { name: 'name', label: 'Название', type: 'text' }, { name: 'course', label: 'Курс', type: 'number' }, { name: 'department_id', label: 'Отдел', type: 'select', ref: 'departments' } ]; case 'subjects': return [ { name: 'name', label: 'Название', type: 'text' }, { name: 'short_name', label: 'Сокращение', type: 'text' }, { name: 'department_id', label: 'Отдел', type: 'select', ref: 'departments' } ]; case 'classrooms': return [ { name: 'room_number', label: 'Номер', type: 'text' }, { name: 'building_id', label: 'Корпус', type: 'select', ref: 'buildings' } ]; case 'teachers': return [ { name: 'person_id', label: 'Физ.лицо', type: 'select', ref: 'persons' }, { name: 'department_id', label: 'Отдел', type: 'select', ref: 'departments' }, { name: 'position_id', label: 'Должность', type: 'select', ref: 'positions' } ]; default: return []; } } // === 1. АВТОРИЗАЦИЯ И UI === function updateAuthUI() { const loginForm = document.getElementById('loginForm'); const userInfo = document.getElementById('userInfo'); const userDisplay = document.getElementById('userDisplay'); const addBtn = document.getElementById('addBtn'); if (authToken && currentUser) { loginForm.classList.add('hidden'); userInfo.classList.remove('hidden'); userDisplay.textContent = `${currentUser.login} (${currentUser.role})`; // Кнопка "Добавить" видна только админу if (currentUser.role === 'admin') { addBtn.classList.remove('hidden'); } else { addBtn.classList.add('hidden'); } } else { loginForm.classList.remove('hidden'); userInfo.classList.add('hidden'); addBtn.classList.add('hidden'); } // Перерисовываем таблицу, чтобы обновить кнопки "Действия" renderTable(); } async function handleLogin() { const loginInput = document.getElementById('loginUsername').value; const passwordInput = document.getElementById('loginPassword').value; try { const res = await fetch(API_LOGIN, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ login: loginInput, password: passwordInput }) }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка входа'); // Сохраняем authToken = data.token; currentUser = { login: data.user.login, role: data.user.role, id: data.user.id }; // Берем инфо из payload, который вернул сервер localStorage.setItem('lab11_token', authToken); localStorage.setItem('lab11_user', JSON.stringify(currentUser)); updateAuthUI(); loadData(); // Перезагрузить данные под новыми правами } catch (e) { alert(e.message); } } function handleLogout() { authToken = null; currentUser = null; localStorage.removeItem('lab11_token'); localStorage.removeItem('lab11_user'); updateAuthUI(); } // Универсальная функция запроса (добавляет токен автоматически) async function apiFetch(url, options = {}) { if (!options.headers) options.headers = {}; if (authToken) { options.headers['Authorization'] = 'Bearer ' + authToken; } const res = await fetch(url, options); if (res.status === 401) { handleLogout(); throw new Error("Сессия истекла. Авторизуйтесь заново."); } if (res.status === 403) { throw new Error("Нет доступа! Нужны права администратора."); } return res; } // === 2. ЗАГРУЗКА И ОТОБРАЖЕНИЕ ДАННЫХ === async function loadData() { const container = document.getElementById('tableContainer'); container.innerHTML = '<p>Загрузка...</p>'; try { // GET запрос const res = await apiFetch(`${API_BASE}/${currentTable}`); if (!res.ok) throw new Error("Ошибка загрузки данных"); currentData = await res.json(); renderTable(); } catch (e) { container.innerHTML = `<p style="color:var(--danger)">${e.message}</p>`; } } function renderTable() { const container = document.getElementById('tableContainer'); if (!currentData || currentData.length === 0) { container.innerHTML = '<p>Нет записей</p>'; return; } const fields = getTableConfig(currentTable); const isAdmin = currentUser && currentUser.role === 'admin'; let html = `<table><thead><tr><th>ID</th>`; // Заголовки полей fields.forEach(f => html += `<th>${f.label}</th>`); // Заголовок действий (только админ) if (isAdmin) html += `<th style="width: 120px">Действия</th>`; html += `</tr></thead><tbody>`; currentData.forEach(item => { html += `<tr><td>${item.id}</td>`; fields.forEach(f => { // Если есть поле, выводим. Если null - пустая строка html += `<td>${item[f.name] ?? ''}</td>`; }); // Кнопки (только админ) if (isAdmin) { html += ` <td> <button class="btn btn-primary btn-sm" onclick="editItem(${item.id})"><i class="fas fa-edit"></i></button> <button class="btn btn-danger btn-sm" onclick="deleteItem(${item.id})"><i class="fas fa-trash"></i></button> </td> `; } html += `</tr>`; }); html += `</tbody></table>`; container.innerHTML = html; } // === 3. РАБОТА С ФОРМОЙ (Create / Edit) === // Хелпер: Получить название для выпадающего списка function getOptionLabel(item) { if (item.title) return item.title; if (item.name) return item.name; if (item.last_name) return `${item.last_name} ${item.first_name}`; return `ID: ${item.id}`; } // Хелпер: Загрузка списка для select (например, список отделов) async function fetchOptions(tableName) { try { const res = await apiFetch(`${API_BASE}/${tableName}`); return await res.json(); } catch { return []; } } async function openModal(id = null) { editingId = id; document.getElementById('modalTitle').textContent = id ? `Редактирование #${id}` : `Новая запись (${currentTable})`; const formFieldsDiv = document.getElementById('formFields'); formFieldsDiv.innerHTML = 'Загрузка полей...'; document.getElementById('modal').style.display = 'flex'; // Ищем данные текущей записи, если редактируем const itemData = id ? currentData.find(i => i.id == id) : {}; const config = getTableConfig(currentTable); let html = ''; for (const field of config) { let inputHtml = ''; if (field.type === 'select') { // Если select, надо загрузить список вариантов const options = await fetchOptions(field.ref); let optsHtml = '<option value="">-- Выберите --</option>'; options.forEach(opt => { const selected = (itemData[field.name] == opt.id) ? 'selected' : ''; optsHtml += `<option value="${opt.id}" ${selected}>${getOptionLabel(opt)}</option>`; }); inputHtml = `<select name="${field.name}" required>${optsHtml}</select>`; } else { // Обычный input inputHtml = `<input type="${field.type}" name="${field.name}" value="${itemData[field.name] || ''}" required>`; } html += ` <div class="form-group"> <label>${field.label}</label> ${inputHtml} </div> `; } formFieldsDiv.innerHTML = html; } function closeModal() { document.getElementById('modal').style.display = 'none'; } // Отправка формы document.getElementById('editForm').onsubmit = async (e) => { e.preventDefault(); const formData = new FormData(e.target); const data = Object.fromEntries(formData.entries()); // Преобразуем пустые строки в null и числа for (let k in data) { if (data[k] === "") delete data[k]; } const url = editingId ? `${API_BASE}/${currentTable}/${editingId}` : `${API_BASE}/${currentTable}`; const method = editingId ? 'PUT' : 'POST'; try { const res = await apiFetch(url, { method: method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Ошибка сохранения'); } closeModal(); loadData(); } catch (e) { alert(e.message); } }; // === 4. УДАЛЕНИЕ === window.editItem = (id) => openModal(id); // Глобальная видимость для onclick в HTML window.deleteItem = async (id) => { if (!confirm('Вы уверены? Это действие нельзя отменить.')) return; try { const res = await apiFetch(`${API_BASE}/${currentTable}/${id}`, { method: 'DELETE' }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || 'Ошибка удаления'); } loadData(); } catch (e) { alert(e.message); } }; // Смена справочника в выпадающем списке document.getElementById('tableSelector').onchange = (e) => { currentTable = e.target.value; loadData(); }; // === ИНИЦИАЛИЗАЦИЯ === updateAuthUI(); loadData(); </script> </body> </html>