/
Cooolprogrammers
/
web-nodejs-labs
Обзор
Документация
Войти
/
Cooolprogrammers
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
public/lab11/task2/shop.html
657 строк
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>Lab 11: Магазин (Full)</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <style> :root { --main-gradient: linear-gradient(135deg, #0a0e14, #111827); --card-gradient: linear-gradient(145deg, #1f2937, #0a0e14); } body { font-family: 'JetBrains Mono', 'Courier New', monospace; margin: 0; min-height: 100vh; background: var(--main-gradient); color: #10b981; padding: 20px; padding-bottom: 80px; background-image: radial-gradient(circle at 10% 20%, rgba(16, 185, 129, 0.05) 0%, transparent 20%), radial-gradient(circle at 90% 80%, rgba(16, 185, 129, 0.05) 0%, transparent 20%); } .container { max-width: 1200px; margin: 0 auto; padding: 30px; background: rgba(31, 41, 55, 0.8); border-radius: 12px; box-shadow: 0 20px 50px rgba(0,0,0,0.3); border: 1px solid #374151; backdrop-filter: blur(10px); } header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; padding: 15px; background: rgba(16, 185, 129, 0.08); border-radius: 8px; border: 1px solid #374151; } h1 { margin: 0; color: #10b981; text-shadow: 0 0 10px rgba(16, 185, 129, 0.3); font-size: 2rem; } .controls { display: flex; gap: 15px; margin-bottom: 25px; flex-wrap: wrap; } button { padding: 12px 20px; border: 1px solid transparent; border-radius: 8px; cursor: pointer; background: transparent; color: #10b981; transition: all 0.3s; font-weight: 600; font-family: 'JetBrains Mono', monospace; letter-spacing: 0.5px; } button:hover { transform: translateY(-2px); box-shadow: 0 6px 15px rgba(16, 185, 129, 0.3); } button.danger { background: transparent; color: #ef4444; border-color: #ef4444; } button.danger:hover { background: rgba(239, 68, 68, 0.1); } button.warning { background: transparent; color: #f59e0b; border-color: #f59e0b; } button.warning:hover { background: rgba(245, 158, 11, 0.1); } button.success { background: transparent; color: #10b981; border-color: #10b981; } button.success:hover { background: rgba(16, 185, 129, 0.1); } button.purple { background: transparent; color: #8b5cf6; border-color: #8b5cf6; } button.purple:hover { background: rgba(139, 92, 246, 0.1); } .products { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 25px; } .card { background: var(--card-gradient); padding: 20px; border-radius: 12px; display: flex; flex-direction: column; justify-content: space-between; position: relative; border: 1px solid #374151; transition: all 0.3s; } /* Подсветка выбранной карточки */ .card.selected { border-color: #10b981; background: rgba(16, 185, 129, 0.08); transform: scale(1.02); box-shadow: 0 8px 32px rgba(16, 185, 129, 0.2); } .price { font-size: 1.5em; font-weight: bold; color: #34d399; margin: 10px 0; } .local-desc { font-style: italic; color: #a7f3d0; background: rgba(16, 185, 129, 0.1); padding: 10px; border-radius: 6px; margin: 8px 0; font-size: 0.9em; border-left: 3px solid #10b981; } /* Элементы управления покупкой */ .buy-controls { margin-top: 15px; display: flex; align-items: center; gap: 10px; padding-top: 15px; border-top: 1px dashed #374151; } .qty-input { width: 60px; padding: 8px; border-radius: 6px; border: 1px solid #374151; text-align: center; background: #0a0e14; color: #a7f3d0; font-family: 'JetBrains Mono', monospace; } .check-buy { width: 20px; height: 20px; cursor: pointer; accent-color: #10b981; } /* Нижняя панель подтверждения */ #bulkPanel { position: fixed; bottom: 0; left: 0; right: 0; background: #1f2937; padding: 20px 40px; box-shadow: 0 -5px 20px rgba(0,0,0,0.4); display: none; justify-content: space-between; align-items: center; z-index: 999; border-top: 1px solid #374151; } #bulkTotal { font-weight: bold; font-size: 1.2em; color: #34d399; } .admin-btn { display: none; } /* Скрыто по умолчанию */ /* Модалка */ .modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.8); justify-content: center; align-items: center; z-index: 1000; backdrop-filter: blur(5px); } .modal-content { background: #1f2937; padding: 30px; border-radius: 12px; width: 400px; animation: slideIn 0.3s; border: 1px solid #374151; box-shadow: 0 20px 40px rgba(0,0,0,0.4); } @keyframes slideIn { from { transform: translateY(-20px); opacity: 0; } to { transform: translateY(0); opacity: 1; } } input[type=text], input[type=number], select, textarea { width: 100%; padding: 12px; margin: 10px 0; border-radius: 8px; border: 1px solid #374151; box-sizing: border-box; background: #0a0e14; color: #a7f3d0; font-family: 'JetBrains Mono', monospace; transition: all 0.3s; } input:focus, select:focus, textarea:focus { outline: none; border-color: #10b981; box-shadow: 0 0 0 3px rgba(16, 185, 129, 0.2); } select { background: #0a0e14; color: #a7f3d0; } h2, h3, h4 { color: #10b981; border-bottom: 1px solid #374151; padding-bottom: 10px; margin-bottom: 15px; } #userInfo { color: #34d399; background: rgba(16, 185, 129, 0.1); padding: 8px 15px; border-radius: 6px; border: 1px solid #374151; } </style> </head> <body> <div class="container"> <header> <button onclick="location.href='../index.html'">← Назад</button> <h1>🛒 Магазин</h1> <div id="userInfo">Гость</div> </header> <div class="controls"> <!-- Кнопки Админа --> <button onclick="openModal('create')" id="btnAdd" class="admin-btn">➕ Добавить товар</button> <button onclick="addCategory()" id="btnCat" class="admin-btn purple">📂 Категория</button> <!-- Фильтр --> <select id="categoryFilter" onchange="loadProducts()" style="padding: 12px; border-radius: 8px; max-width: 200px; border: 1px solid #374151; background: #0a0e14; color: #a7f3d0;"> <option value="">Все категории</option> </select> </div> <div id="products" class="products"></div> </div> <!-- НИЖНЯЯ ПАНЕЛЬ ДЛЯ ПОКУПКИ (ЮЗЕР) --> <div id="bulkPanel"> <div style="color: #a7f3d0;"> Выбрано товаров: <b id="selectedCount" style="color:#10b981">0</b>. Итого: <b id="totalPriceDisplay" style="font-size: 1.3em; color:#34d399;">0</b> ₽ </div> <button class="success" onclick="buySelected()">🛒 Оформить заказ</button> </div> <!-- МОДАЛКА (Создание/Редактирование) --> <div id="modalProduct" class="modal"> <div class="modal-content"> <h2 id="modalTitle">Товар</h2> <input type="hidden" id="editProductId"> <label>Название:</label> <input type="text" id="inpTitle"> <label>Цена:</label> <input type="number" id="inpPrice"> <label>Количество на складе:</label> <input type="number" id="inpAmount" value="10"> <label>Категория:</label> <select id="inpCategory"></select> <label>Описание (Личная заметка):</label> <textarea id="inpDesc" rows="3"></textarea> <div style="display: flex; gap: 10px; margin-top: 20px;"> <button onclick="saveProduct()" class="success" style="flex:1">💾 Сохранить</button> <button onclick="closeModal()" style="flex:1; background:transparent; color:#9ca3af; border-color:#4b5563">❌ Отмена</button> </div> </div> </div> <script> const API_BASE = '/api/lab11/shop'; let token = localStorage.getItem('token'); let userRole = null; const LS_KEY = 'shop_descriptions'; // Кэш товаров для подсчета цены let loadedProducts = []; // --- LOCAL STORAGE HELPERS --- function getLocalDesc(id) { const store = JSON.parse(localStorage.getItem(LS_KEY) || '{}'); return store[id] || ''; } function saveLocalDesc(id, text) { const store = JSON.parse(localStorage.getItem(LS_KEY) || '{}'); if (text && text.trim()) store[id] = text; else delete store[id]; localStorage.setItem(LS_KEY, JSON.stringify(store)); } function removeLocalDesc(id) { const store = JSON.parse(localStorage.getItem(LS_KEY) || '{}'); delete store[id]; localStorage.setItem(LS_KEY, JSON.stringify(store)); } // --- 1. ИНИЦИАЛИЗАЦИЯ --- if (token) { try { const payload = JSON.parse(atob(token.split('.')[1])); userRole = payload.role; document.getElementById('userInfo').textContent = `${payload.login} (${userRole})`; if (userRole === 'admin') { // Показываем кнопки админа document.querySelectorAll('.admin-btn').forEach(e => e.style.display = 'inline-block'); } } catch (e) { localStorage.removeItem('token'); } } // --- 2. ЗАГРУЗКА ДАННЫХ --- async function loadCategories() { try { const res = await fetch(`${API_BASE}/categories`); const cats = await res.json(); const filter = document.getElementById('categoryFilter'); const modalSelect = document.getElementById('inpCategory'); // Сохраняем текущий выбор фильтра const currentVal = filter.value; // Очистка (кроме первой опции в фильтре) filter.innerHTML = '<option value="">Все категории</option>'; modalSelect.innerHTML = ''; cats.forEach(c => { filter.innerHTML += `<option value="${c.id}">${c.name}</option>`; modalSelect.innerHTML += `<option value="${c.id}">${c.name}</option>`; }); filter.value = currentVal; } catch(e) { console.error(e); } } async function loadProducts() { const catId = document.getElementById('categoryFilter').value; let url = `${API_BASE}/products?limit=100`; if (catId) url += `&category=${catId}`; try { const res = await fetch(url); const result = await res.json(); loadedProducts = result.data || []; renderProducts(); updateBulkPanel(); } catch(e) { console.error(e); } } function renderProducts() { const list = document.getElementById('products'); list.innerHTML = ''; if (loadedProducts.length === 0) { list.innerHTML = '<p style="color:#9ca3af; text-align:center;">Товаров нет</p>'; return; } loadedProducts.forEach(p => { const description = getLocalDesc(p.id); const descHtml = description ? `<div class="local-desc">${description}</div>` : ''; let actionsHTML = ''; let userControlsHTML = ''; // ЛОГИКА АДМИНА if (userRole === 'admin') { actionsHTML = ` <div style="margin-top:15px; display:flex; gap:8px;"> <button class="warning" style="flex:1" onclick='openModal("edit", ${JSON.stringify(p)}, "${description.replace(/"/g, '"')}")'>✏️ Edit</button> <button class="danger" style="flex:1" onclick="deleteProduct(${p.id})">🗑️ Del</button> </div> `; } // ЛОГИКА ЮЗЕРА (Массовая покупка) else if (userRole === 'user') { const disabled = p.amount <= 0 ? 'disabled' : ''; userControlsHTML = ` <div class="buy-controls"> <input type="checkbox" class="check-buy" data-id="${p.id}" ${disabled} onchange="toggleSelection(this)"> <label style="color:#a7f3d0;">Купить:</label> <input type="number" class="qty-input" id="qty-${p.id}" value="1" min="1" max="${p.amount}" ${disabled} onchange="updateBulkPanel()"> <span style="color:#9ca3af;">шт.</span> </div> `; } else { actionsHTML = `<div style="margin-top:10px; color:#ef4444; font-size:0.9em">Войдите для покупок</div>`; } const html = ` <div class="card" id="card-${p.id}"> <div> <h3>${p.title}</h3> <div style="font-size:0.85em; color:#9ca3af; margin-bottom:5px;">${p.category_title || 'Без категории'}</div> <div class="price">${p.price} ₽</div> <p style="margin:0; color:#a7f3d0;">Остаток: <b style="color:#${p.amount > 0 ? '34d399' : 'ef4444'}">${p.amount}</b></p> ${descHtml} </div> ${userControlsHTML} ${actionsHTML} </div> `; list.innerHTML += html; }); } // --- 3. ФУНКЦИОНАЛ ПОКУПКИ (USER) --- function toggleSelection(checkbox) { const card = document.getElementById(`card-${checkbox.dataset.id}`); if (checkbox.checked) card.classList.add('selected'); else card.classList.remove('selected'); updateBulkPanel(); } function updateBulkPanel() { const checkboxes = document.querySelectorAll('.check-buy:checked'); const panel = document.getElementById('bulkPanel'); if (checkboxes.length > 0) { panel.style.display = 'flex'; let count = 0; let totalCost = 0; checkboxes.forEach(cb => { const id = parseInt(cb.dataset.id); const qtyInput = document.getElementById(`qty-${id}`); const qty = parseInt(qtyInput.value) || 1; const product = loadedProducts.find(p => p.id === id); if (product) { count += qty; totalCost += product.price * qty; } }); document.getElementById('selectedCount').innerText = count; document.getElementById('totalPriceDisplay').innerText = totalCost; } else { panel.style.display = 'none'; } } async function buySelected() { const checkboxes = document.querySelectorAll('.check-buy:checked'); if (checkboxes.length === 0) return; if (!confirm('Подтвердить покупку?')) return; const itemsToBuy = []; checkboxes.forEach(cb => { const id = cb.dataset.id; const qty = document.getElementById(`qty-${id}`).value; itemsToBuy.push({ id: id, quantity: qty }); }); try { const res = await fetch(`${API_BASE}/buy`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ items: itemsToBuy }) }); const data = await res.json(); if (res.ok) { let msg = "Результат заказа:\n"; if (data.details.bought.length) msg += "✅ Куплено:\n" + data.details.bought.join('\n') + "\n"; if (data.details.failed.length) msg += "❌ Ошибки:\n" + data.details.failed.join('\n'); alert(msg); loadProducts(); } else { alert('❌ Ошибка заказа: ' + data.error); } } catch (e) { alert('❌ Ошибка сети'); } } // --- 4. ФУНКЦИОНАЛ АДМИНА --- // Добавление категории async function addCategory() { const name = prompt("Введите название новой категории:"); if (!name) return; try { const res = await fetch(`${API_BASE}/categories`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify({ name: name }) }); if (res.ok) { alert('✅ Категория создана'); loadCategories(); } else alert('❌ Ошибка создания категории'); } catch(e) { console.error(e); } } // Модалка товара function openModal(mode, product = null, localDesc = '') { const modal = document.getElementById('modalProduct'); const title = document.getElementById('modalTitle'); if (mode === 'create') { title.innerText = "🆕 Новый товар"; document.getElementById('editProductId').value = ''; document.getElementById('inpTitle').value = ''; document.getElementById('inpPrice').value = ''; document.getElementById('inpAmount').value = '10'; document.getElementById('inpDesc').value = ''; } else { title.innerText = "✏️ Редактирование"; document.getElementById('editProductId').value = product.id; document.getElementById('inpTitle').value = product.title; document.getElementById('inpPrice').value = product.price; document.getElementById('inpAmount').value = product.amount; document.getElementById('inpCategory').value = product.category_id; document.getElementById('inpDesc').value = localDesc || getLocalDesc(product.id); } modal.style.display = 'flex'; } function closeModal() { document.getElementById('modalProduct').style.display = 'none'; } // Сохранение товара async function saveProduct() { const id = document.getElementById('editProductId').value; const desc = document.getElementById('inpDesc').value; const data = { title: document.getElementById('inpTitle').value, price: document.getElementById('inpPrice').value, amount: document.getElementById('inpAmount').value, category_id: document.getElementById('inpCategory').value }; let method = 'POST'; let url = `${API_BASE}/products`; if (id) { method = 'PUT'; url = `${API_BASE}/products/${id}`; } try { const res = await fetch(url, { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` }, body: JSON.stringify(data) }); if(res.ok) { if(!id) { const created = await res.json(); saveLocalDesc(created.id, desc); } else { saveLocalDesc(id, desc); } closeModal(); loadProducts(); } else { const err = await res.json(); alert('❌ Ошибка: ' + (err.error || 'Server error')); } } catch(e) { console.error(e); } } // Удаление async function deleteProduct(id) { if(!confirm('Удалить товар?')) return; const res = await fetch(`${API_BASE}/products/${id}`, { method: 'DELETE', headers: { 'Authorization': `Bearer ${token}` } }); if (res.ok) { removeLocalDesc(id); loadProducts(); } } // START loadCategories(); loadProducts(); </script> </body> </html>