/
Cooolprogrammers
/
web-nodejs-labs
Обзор
Документация
Войти
/
Cooolprogrammers
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
public/lab11/task2/shop.htm
429 строк
17 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, #667eea, #764ba2, #f093fb); --card-gradient: linear-gradient(145deg, #ffffff, #e6e9f0); } body { font-family: 'Inter', sans-serif; margin: 0; min-height: 100vh; background: var(--main-gradient); color: #222; padding: 20px; padding-bottom: 80px;} .container { max-width: 1200px; margin: 0 auto; padding: 25px; background: rgba(255,255,255,0.95); border-radius: 28px; box-shadow: 0 20px 50px rgba(0,0,0,0.2); } header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px; } h1 { margin: 0; background: linear-gradient(45deg, #007bff, #6610f2); -webkit-background-clip: text; -webkit-text-fill-color: transparent; } .controls { display: flex; gap: 10px; margin-bottom: 20px; flex-wrap: wrap; } button { padding: 10px 20px; border: none; border-radius: 12px; cursor: pointer; background: #007bff; color: white; transition: 0.2s; font-weight: 500;} button:hover { transform: translateY(-2px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); } button.danger { background: #dc3545; } button.warning { background: #ffc107; color: black; } button.success { background: #28a745; } button.purple { background: #6f42c1; } .products { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; } .card { background: var(--card-gradient); padding: 20px; border-radius: 20px; display: flex; flex-direction: column; justify-content: space-between; position: relative; border: 2px solid transparent; transition: 0.2s;} /* Подсветка выбранной карточки */ .card.selected { border-color: #007bff; background: #f0f8ff; transform: scale(1.02); } .price { font-size: 1.4em; font-weight: bold; color: #0072ff; margin: 10px 0; } .local-desc { font-style: italic; color: #555; background: #f9f9f9; padding: 8px; border-radius: 8px; margin: 5px 0; font-size: 0.9em; border-left: 3px solid #ccc;} /* Элементы управления покупкой */ .buy-controls { margin-top: 15px; display: flex; align-items: center; gap: 10px; padding-top: 10px; border-top: 1px dashed #ccc; } .qty-input { width: 60px; padding: 5px; border-radius: 5px; border: 1px solid #ccc; text-align: center; } .check-buy { width: 20px; height: 20px; cursor: pointer; } /* Нижняя панель подтверждения */ #bulkPanel { position: fixed; bottom: 0; left: 0; right: 0; background: white; padding: 20px 40px; box-shadow: 0 -5px 20px rgba(0,0,0,0.15); display: none; justify-content: space-between; align-items: center; z-index: 999; } #bulkTotal { font-weight: bold; font-size: 1.2em; } .admin-btn { display: none; } /* Скрыто по умолчанию */ /* Модалка */ .modal { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.6); justify-content: center; align-items: center; z-index: 1000; } .modal-content { background: white; padding: 30px; border-radius: 20px; width: 400px; animation: slideIn 0.3s; } @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: 8px 0; border-radius: 8px; border: 1px solid #ccc; box-sizing: border-box; } </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: 10px; border-radius: 12px; max-width: 200px;"> <option value="">Все категории</option> </select> </div> <div id="products" class="products"></div> </div> <!-- НИЖНЯЯ ПАНЕЛЬ ДЛЯ ПОКУПКИ (ЮЗЕР) --> <div id="bulkPanel"> <div> Выбрано товаров: <b id="selectedCount" style="color:#007bff">0</b>. Итого: <b id="totalPriceDisplay" style="font-size: 1.3em;">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: 15px;"> <button onclick="saveProduct()" class="success" style="flex:1">Сохранить</button> <button onclick="closeModal()" style="flex:1; background:#ccc; color:#333">Отмена</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>Товаров нет</p>'; return; } loadedProducts.forEach(p => { // Получаем описание из LS 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:5px;"> <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>Купить:</label> <input type="number" class="qty-input" id="qty-${p.id}" value="1" min="1" max="${p.amount}" ${disabled} onchange="updateBulkPanel()"> <span>шт.</span> </div> `; } else { actionsHTML = `<div style="margin-top:10px; color:red; 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:#777; margin-bottom:5px;">${p.category_title || 'Без категории'}</div> <div class="price">${p.price} ₽</div> <p style="margin:0;">Остаток: <b>${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; // Загружаем описание из переданного параметра или из LS 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) { // При создании берем 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>