/
Cooolprogrammers
/
web-nodejs-labs
Обзор
Документация
Войти
/
Cooolprogrammers
/
web-nodejs-labs
Код
Запросы
0
Задачи
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
public/lab9/task2/index.html
457 строк
18 KB
darkvoid6@mail.ru
ЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫЫ
18 дек 2025, 07:59
18 дек 2025, 07:59
f2c4f06
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8"> <title>ЛР9 - Задание 2 - Управление товарами</title> <style> body { font-family: 'Courier New', monospace; max-width: 1400px; margin: 2em auto; padding: 0 20px; background: #0a0a0a; color: #00ff41; } body::before { content: ''; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: linear-gradient(to bottom, transparent 90%, rgba(0, 255, 65, 0.03)); pointer-events: none; z-index: 1; } h1, h2, h3 { color: #00ff41; text-shadow: 0 0 10px #00ff41; letter-spacing: 1px; text-transform: uppercase; } h1 { border-bottom: 2px solid #00ff41; padding-bottom: 10px; } h2 { color: #00cc33; margin-bottom: 30px; } h3 { border-bottom: 1px solid #00ff41; padding-bottom: 8px; margin-bottom: 15px; } .stats-panel, .search-panel, .controls-panel { background: linear-gradient(135deg, #001a00, #003300); padding: 20px; margin-bottom: 20px; border: 2px solid #00ff41; box-shadow: 0 0 20px rgba(0, 255, 65, 0.2); } .stats-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 15px; margin-top: 15px; } .stat-card { background: rgba(0, 20, 0, 0.9); padding: 20px; text-align: center; border: 1px solid #00ff41; } .stat-value { font-size: 24px; font-weight: bold; color: #00ff41; text-shadow: 0 0 10px #00ff41; } .stat-label { color: #00cc33; margin-top: 5px; font-size: 0.9em; } .controls-panel { display: flex; justify-content: space-between; align-items: center; } .search-input { padding: 10px; border: 2px solid #006600; font-size: 16px; background: #001a00; color: #00ff41; font-family: 'Courier New', monospace; width: 200px; } .search-input:focus { outline: none; border-color: #00ff41; box-shadow: 0 0 15px rgba(0, 255, 65, 0.3); } .products-table { width: 100%; border-collapse: collapse; margin-top: 20px; background: rgba(0, 20, 0, 0.9); box-shadow: 0 1px 3px rgba(0, 255, 65, 0.1); } .products-table th { background: #001a00; color: #00ff41; padding: 12px; text-align: left; font-weight: bold; border-bottom: 2px solid #00ff41; } .products-table td { padding: 12px; border-bottom: 1px solid #006600; vertical-align: middle; } .products-table tr:hover { background-color: rgba(0, 50, 0, 0.5); } .editable-field { border: 1px solid #00ff41; padding: 6px 8px; width: 100%; box-sizing: border-box; background: #001a00; color: #00ff41; font-family: 'Courier New', monospace; } .amount-controls { display: flex; align-items: center; gap: 8px; } .amount-btn { width: 30px; height: 30px; border: 1px solid #00ff41; background: #001a00; color: #00ff41; cursor: pointer; font-weight: bold; } .amount-btn:hover:not(:disabled) { background: #00ff41; color: #000; } .amount-btn:disabled { border-color: #006600; color: #006600; } .amount-display { min-width: 40px; text-align: center; font-weight: bold; } .save-btn, .search-btn, .clear-btn, .action-btn { padding: 10px 20px; border: 1px solid #00ff41; cursor: pointer; font-family: 'Courier New', monospace; text-transform: uppercase; letter-spacing: 1px; transition: all 0.3s ease; } .save-btn { background: #003300; color: #00ff41; } .save-btn:hover:not(:disabled) { background: #00ff41; color: #000; box-shadow: 0 0 15px rgba(0, 255, 65, 0.3); } .save-btn:disabled { border-color: #006600; color: #006600; cursor: not-allowed; } .search-btn { background: #002222; color: #00ff41; } .search-btn:hover { background: #00ff41; color: #000; } .clear-btn { background: #001a00; color: #00cc33; border-color: #00cc33; } .clear-btn:hover { background: #00cc33; color: #000; } .action-btn { padding: 6px 12px; font-size: 12px; background: #003300; color: #00ff41; border-color: #00ff41; } .action-btn:hover { background: #00ff41; color: #000; } .loading { text-align: center; padding: 40px; color: #00cc33; font-style: italic; border: 1px dashed #006600; } .error { background: rgba(50, 0, 0, 0.8); color: #ff3333; padding: 15px; border: 1px solid #ff3333; margin: 10px 0; } .success { background: rgba(0, 50, 0, 0.8); color: #00ff41; padding: 15px; border: 1px solid #00ff41; margin: 10px 0; } .highlighted-row { background-color: rgba(0, 255, 65, 0.1) !important; } @media (max-width: 768px) { .stats-grid { grid-template-columns: 1fr; } .controls-panel { flex-direction: column; gap: 10px; } } </style> </head> <body> <h1>ЛР9 - ЗАДАНИЕ 2</h1> <h2>🎛️ УПРАВЛЕНИЕ ТОВАРАМИ</h2> <div class="stats-panel"> <h3>📈 СТАТИСТИКА СКЛАДА</h3> <div class="stats-grid" id="statsContainer"> <div class="loading">ЗАГРУЗКА...</div> </div> </div> <div class="search-panel"> <h3>🔍 ПОИСК ПО ID</h3> <div style="display: flex; gap: 10px; align-items: center;"> <input type="number" id="searchByIdInput" class="search-input" placeholder="ID ТОВАРА"> <button onclick="searchById()" class="search-btn">НАЙТИ</button> <button onclick="clearSearch()" class="clear-btn">ВСЕ ТОВАРЫ</button> </div> <div id="searchResult" style="margin-top: 10px;"></div> </div> <div class="controls-panel"> <h3 style="margin: 0;">📦 УПРАВЛЕНИЕ</h3> <button class="save-btn" id="saveAllBtn" disabled onclick="saveAllChanges()"> 💾 СОХРАНИТЬ </button> </div> <div id="productsContainer"> <div class="loading">ЗАГРУЗКА...</div> </div> <script> const API_BASE = '/api/lab9'; const TASK2_API = '/api/lab9/task2'; let changedProducts = new Map(); let allProducts = []; function formatPrice(cents) { return (cents / 100).toLocaleString('ru-RU', {style: 'currency', currency: 'RUB'}); } function formatTotalValue(cents) { const rubles = cents / 100; if (rubles >= 1000000) return (rubles / 1000000).toFixed(1) + ' МЛН ₽'; if (rubles >= 1000) return (rubles / 1000).toFixed(0) + ' ТЫС ₽'; return formatPrice(cents); } function parsePriceToCents(priceText) { const cleanText = priceText.replace(/[^\d.,]/g, '').replace(',', '.'); return Math.round((parseFloat(cleanText) || 0) * 100); } function formatPriceForInput(cents) { return (cents / 100).toFixed(2); } async function loadStats() { try { const response = await fetch(`${TASK2_API}/stock`); const stats = await response.json(); document.getElementById('statsContainer').innerHTML = ` <div class="stat-card"><div class="stat-value">${stats.total_products || 0}</div><div class="stat-label">НАИМЕНОВАНИЙ</div></div> <div class="stat-card"><div class="stat-value">${stats.total_items || 0}</div><div class="stat-label">ЕДИНИЦ</div></div> <div class="stat-card"><div class="stat-value">${formatTotalValue(stats.total_value || 0)}</div><div class="stat-label">СТОИМОСТЬ</div></div> `; } catch (error) { document.getElementById('statsContainer').innerHTML = `<div class="error">ОШИБКА: ${error.message}</div>`; } } async function searchById() { const productId = document.getElementById('searchByIdInput').value.trim(); const resultDiv = document.getElementById('searchResult'); if (!productId) { resultDiv.innerHTML = '<div class="error">❌ ВВЕДИТЕ ID</div>'; return; } resultDiv.innerHTML = '<div class="loading">ПОИСК...</div>'; try { const response = await fetch(`${API_BASE}/products/${productId}`); if (response.ok) { const product = await response.json(); highlightProduct(product.id); resultDiv.innerHTML = `<div class="success">✅ ${product.title} | ${formatPrice(product.price)} | ${product.amount} ШТ.</div>`; } else if (response.status === 404) { resultDiv.innerHTML = '<div class="error">❌ НЕ НАЙДЕНО</div>'; } else { resultDiv.innerHTML = '<div class="error">❌ ОШИБКА</div>'; } } catch (error) { resultDiv.innerHTML = `<div class="error">❌ ОШИБКА: ${error.message}</div>`; } } function highlightProduct(productId) { document.querySelectorAll('.highlighted-row').forEach(row => row.classList.remove('highlighted-row')); const rows = document.querySelectorAll('.products-table tbody tr'); rows.forEach(row => { const idCell = row.querySelector('td:first-child'); if (idCell && idCell.textContent.trim() === productId.toString()) { row.classList.add('highlighted-row'); row.scrollIntoView({ behavior: 'smooth', block: 'center' }); setTimeout(() => row.classList.remove('highlighted-row'), 5000); } }); } function clearSearch() { document.getElementById('searchByIdInput').value = ''; document.getElementById('searchResult').innerHTML = ''; document.querySelectorAll('.highlighted-row').forEach(row => row.classList.remove('highlighted-row')); renderProductsTable(allProducts); } async function loadProducts() { const container = document.getElementById('productsContainer'); container.innerHTML = '<div class="loading">ЗАГРУЗКА...</div>'; try { const response = await fetch(`${API_BASE}/products`); allProducts = await response.json(); renderProductsTable(allProducts); } catch (error) { container.innerHTML = `<div class="error">ОШИБКА: ${error.message}</div>`; } } function renderProductsTable(products) { const container = document.getElementById('productsContainer'); if (products.length === 0) { container.innerHTML = '<div class="loading">НЕ НАЙДЕНО</div>'; return; } container.innerHTML = ` <table class="products-table"> <thead><tr><th>ID</th><th>НАЗВАНИЕ</th><th>ЦЕНА</th><th>КОЛ-ВО</th><th>ДЕЙСТВИЯ</th></tr></thead> <tbody> ${products.map(product => { const currentProduct = changedProducts.get(product.id) || product; return ` <tr id="product-row-${product.id}"> <td>${product.id}</td> <td><input type="text" class="editable-field" value="${currentProduct.title}" onchange="onFieldChange(${product.id}, 'title', this.value)"></td> <td><input type="text" class="editable-field" value="${formatPriceForInput(currentProduct.price)}" onchange="onFieldChange(${product.id}, 'price', this.value)"></td> <td> <div class="amount-controls"> <button class="amount-btn" onclick="changeAmount(${product.id}, -1)" ${currentProduct.amount <= 0 ? 'disabled' : ''}>-</button> <span class="amount-display">${currentProduct.amount}</span> <button class="amount-btn" onclick="changeAmount(${product.id}, 1)">+</button> </div> </td> <td><button class="action-btn" onclick="resetProduct(${product.id})">🔄 СБРОС</button></td> </tr> `; }).join('')} </tbody> </table> `; } function changeAmount(productId, delta) { const product = allProducts.find(p => p.id === productId); if (!product) return; const changedProduct = changedProducts.get(productId) || {...product}; const newAmount = changedProduct.amount + delta; if (newAmount >= 0) { changedProduct.amount = newAmount; changedProducts.set(productId, changedProduct); updateSaveButton(); renderProductsTable(allProducts); } } function onFieldChange(productId, field, value) { const product = allProducts.find(p => p.id === productId); if (!product) return; const changedProduct = changedProducts.get(productId) || {...product}; if (field === 'price') changedProduct.price = parsePriceToCents(value); else changedProduct[field] = value; changedProducts.set(productId, changedProduct); updateSaveButton(); } function resetProduct(productId) { changedProducts.delete(productId); updateSaveButton(); renderProductsTable(allProducts); } function updateSaveButton() { const saveBtn = document.getElementById('saveAllBtn'); saveBtn.disabled = changedProducts.size === 0; saveBtn.textContent = changedProducts.size > 0 ? `💾 СОХРАНИТЬ (${changedProducts.size})` : '💾 СОХРАНИТЬ'; } async function saveAllChanges() { const saveBtn = document.getElementById('saveAllBtn'); saveBtn.disabled = true; saveBtn.textContent = '⏳ СОХРАНЕНИЕ...'; try { const updates = Array.from(changedProducts.values()); const promises = updates.map(product => fetch(`${API_BASE}/products/${product.id}`, { method: 'PUT', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({title: product.title, price: product.price, amount: product.amount}) }) ); const results = await Promise.all(promises); if (results.every(response => response.ok)) { showMessage('✅ СОХРАНЕНО!', 'success'); changedProducts.clear(); updateSaveButton(); await loadProducts(); await loadStats(); } else throw new Error('ОШИБКА СОХРАНЕНИЯ'); } catch (error) { showMessage(`❌ ОШИБКА: ${error.message}`, 'error'); } finally { saveBtn.disabled = false; updateSaveButton(); } } function showMessage(message, type) { const messageDiv = document.createElement('div'); messageDiv.className = type === 'success' ? 'success' : 'error'; messageDiv.textContent = message; document.body.insertBefore(messageDiv, document.querySelector('.stats-panel').nextSibling); setTimeout(() => messageDiv.remove(), 5000); } window.addEventListener('load', async () => { await loadStats(); await loadProducts(); }); </script> </body> </html>