/
erioxis
/
web-nodejs
Обзор
Документация
Войти
/
erioxis
/
web-nodejs
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
master
public/lab9/task2/index.html
352 строки
14 KB
erioxis
lab11fix
15 дек 2025, 23:21
15 дек 2025, 23:21
70dc7ba
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8" /> <title>ЛР9 — Задание 2</title> <style> * { margin:0; padding:0; box-sizing:border-box; } html, body { height:100%; font-family:'Courier New', monospace; background:#fff; color:#000; overflow-x:hidden; } body { display:flex; flex-direction:column; padding:20px; } .wrapper { flex:1 0 auto; } .header { text-align:center; margin-bottom:20px; padding:10px 0; border-bottom:1px solid #000; } .header h1 { font-family:'Georgia',serif; font-size:1.8rem; font-weight:normal; letter-spacing:0.1em; text-transform:uppercase; } .container { max-width:1000px; margin:0 auto; padding:20px; background:#fff; border:2px solid #000; } .stock-info { margin-bottom:20px; padding:10px; background:#fff; border:1px dashed #ccc; font-weight:bold; } .input-group { margin:10px 0; } input[type="text"], input[type="number"] { padding:6px; border:1px solid #000; font-family:'Courier New',monospace; margin-right:6px; } .price-input { width:150px; } .product-card { border:1px solid #000; padding:12px; margin:10px 0; display:flex; justify-content:space-between; align-items:center; } .editable-group { display:flex; align-items:center; gap:10px; } .amount-controls { display:flex; align-items:center; gap:6px; } .amount-btn { width:24px; text-align:center; padding:0; border:1px solid #000; background:#fff; cursor:pointer; } .amount-btn:hover { background:#000; color:#fff; } button { padding:6px 12px; border:1px solid #000; background:#fff; color:#000; cursor:pointer; font-family:'Courier New',monospace; margin-left:10px; } button:hover { background:#000; color:#fff; } /* Стили для таблицы */ table { width: 100%; border-collapse: collapse; margin: 20px 0; } th, td { padding: 10px; text-align: left; border: 1px solid #000; } th { background-color: #f0f0f0; } .btn-secondary { padding: 4px 8px; border: 1px solid #000; background: #fff; cursor: pointer; } .btn-secondary:hover { background: #000; color: #fff; } .delete-btn { width: 24px; height: 24px; padding: 0; border: 1px solid #000; background: #fff; color: #000; cursor: pointer; font-size: 16px; } .delete-btn:hover { background: #000; color: #fff; } .editable-field { cursor: pointer; } .editable-field:focus { outline: 1px solid #000; } .inline-edit-input { width: 100%; padding: 4px; border: 1px solid #000; font-family: 'Courier New', monospace; } .error { color: red; } .stats-container { margin-bottom: 20px; padding: 10px; background: #f9f9f9; border: 1px solid #ccc; } </style> </head> <body> <div class="wrapper"> <div class="header"> <h1>Задание 2 — Редактирование</h1> </div> <div class="container"> <!-- Статистика --> <div class="stats-container" id="statsContainer">Загрузка статистики...</div> <!-- Поиск по ID --> <div class="input-group"> <input type="number" id="productId" placeholder="ID товара" min="1" style="width:120px;"> <button onclick="searchProductById()">Показать товар</button> <button onclick="saveAllProducts()">Сохранить все изменения</button> </div> <div id="productDetails"></div> <!-- Таблица товаров --> <table> <thead> <tr> <th>ID</th> <th>Название</th> <th>Цена</th> <th>Количество</th> <th>Действия</th> </tr> </thead> <tbody id="productsTableBody"> <tr><td colspan="5">Загрузка...</td></tr> </tbody> </table> </div> </div> <script> const API = '/api/lab9/products'; const API_TASK1_STOCK = '/api/lab9/task1/stock'; const formatPrice = (cents) => { if (isNaN(cents)) return 'N/A'; return (cents / 100).toLocaleString('ru-RU', { style: 'currency', currency: 'RUB', maximumFractionDigits: 2 }); }; const pendingChanges = {}; // Загрузка и отображение статистики async function loadStockStats() { try { const res = await fetch(API_TASK1_STOCK); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || `HTTP error! status: ${res.status}`); } const stats = await res.json(); document.getElementById('statsContainer').innerHTML = ` <p><strong>Всего наименований:</strong> ${stats.total_products}</p> <p><strong>Общее количество единиц на складе:</strong> ${stats.total_items}</p> <p><strong>Общая стоимость:</strong> ${formatPrice(stats.total_value)}</p> `; } catch (error) { console.error('Ошибка загрузки статистики:', error); document.getElementById('statsContainer').innerHTML = `<p class="error">Ошибка загрузки статистики: ${error.message}</p>`; } } // Загрузка и отображение товаров async function loadProducts() { try { const res = await fetch(API); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || `HTTP error! status: ${res.status}`); } const products = await res.json(); const tableBody = document.getElementById('productsTableBody'); tableBody.innerHTML = ''; if (products.length === 0) { tableBody.innerHTML = '<tr><td colspan="5">Товары не найдены.</td></tr>'; return; } Object.keys(pendingChanges).forEach(key => delete pendingChanges[key]); products.forEach(product => { const row = document.createElement('tr'); row.setAttribute('data-id', product.id); pendingChanges[product.id] = { title: product.title, price: product.price, amount: product.amount }; row.innerHTML = ` <td>${product.id}</td> <td> <div class="editable-field" data-field="title" data-id="${product.id}"> ${product.title} </div> </td> <td> <div class="editable-field" data-field="price" data-id="${product.id}"> ${formatPrice(product.price)} </div> </td> <td> <div class="amount-control" data-id="${product.id}"> <button type="button" class="btn-secondary" onclick="changeAmount(${product.id}, -1)">-</button> <span class="amount-display" style="margin: 0 10px; min-width: 40px; display: inline-block; text-align: center;">${product.amount}</span> <button type="button" class="btn-secondary" onclick="changeAmount(${product.id}, 1)">+</button> </div> </td> <td> <button type="button" class="delete-btn" onclick="deleteProduct(${product.id})">×</button> </td> `; tableBody.appendChild(row); }); setupInlineEditing(); } catch (error) { console.error('Ошибка загрузки товаров:', error); document.getElementById('productsTableBody').innerHTML = `<tr><td colspan="5" class="error">Ошибка загрузки товаров: ${error.message}</td></tr>`; } } // Поиск товара по ID async function searchProductById() { const id = document.getElementById('productId').value.trim(); if (!id || isNaN(id)) { alert('Пожалуйста, введите корректный ID товара'); return; } try { const res = await fetch(`${API}/${id}`); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || `HTTP error! status: ${res.status}`); } const product = await res.json(); document.getElementById('productDetails').innerHTML = ` <div style="margin-top: 15px; padding: 10px; background: rgba(0, 0, 0, 0.2); border-radius: 8px;"> <p><strong>ID:</strong> ${product.id}</p> <p><strong>Название:</strong> ${product.title}</p> <p><strong>Цена:</strong> ${formatPrice(product.price)}</p> <p><strong>Остаток:</strong> ${product.amount} шт.</p> </div> `; } catch (error) { console.error('Ошибка поиска товара:', error); document.getElementById('productDetails').innerHTML = `<p class="error">Ошибка поиска товара: ${error.message}</p>`; } } // Удаление товара async function deleteProduct(id) { if (!confirm('Удалить товар?')) return; try { const res = await fetch(`${API}/${id}`, { method: 'DELETE' }); if (!res.ok) { const errorData = await res.json(); throw new Error(errorData.error || `HTTP error! status: ${res.status}`); } loadProducts(); loadStockStats(); } catch (error) { console.error('Ошибка удаления товара:', error); alert(`Ошибка удаления: ${error.message}`); } } // Изменение количества товара function changeAmount(id, delta) { const amountElement = document.querySelector(`.amount-control[data-id="${id}"] .amount-display`); const currentAmount = parseInt(amountElement.textContent); const newAmount = Math.max(0, currentAmount + delta); amountElement.textContent = newAmount; if (!pendingChanges[id]) { pendingChanges[id] = {}; } pendingChanges[id].amount = newAmount; } // Инлайн-редактирование function setupInlineEditing() { document.querySelectorAll('.editable-field').forEach(element => { element.removeEventListener('click', inlineEditHandler); // Убираем дублирование обработчиков element.addEventListener('click', inlineEditHandler); }); } function inlineEditHandler() { const field = this.getAttribute('data-field'); const id = this.getAttribute('data-id'); const currentValue = pendingChanges[id][field]; const input = document.createElement('input'); input.type = field === 'price' ? 'number' : 'text'; input.step = field === 'price' ? '0.01' : undefined; input.value = field === 'price' ? (currentValue / 100).toFixed(2) : currentValue; input.className = 'inline-edit-input'; this.innerHTML = ''; this.appendChild(input); input.focus(); const finishEditing = () => { let newValue = input.value.trim(); if (field === 'price') { newValue = parseFloat(newValue); if (!isNaN(newValue) && newValue > 0) { pendingChanges[id].price = Math.round(newValue * 100); this.textContent = formatPrice(pendingChanges[id].price); } else { this.textContent = formatPrice(pendingChanges[id].price); } } else { if (newValue) { pendingChanges[id].title = newValue; this.textContent = newValue; } else { this.textContent = pendingChanges[id].title; } } setupInlineEditing(); // Перезапуск обработчиков после редактирования }; input.addEventListener('blur', finishEditing); input.addEventListener('keypress', (e) => { if (e.key === 'Enter') finishEditing(); }); } // Сохранение всех изменений async function saveAllProducts() { const productIds = Object.keys(pendingChanges); if (productIds.length === 0) { alert('Нет изменений для сохранения'); return; } let hasChanges = false; for (const id of productIds) { // Загружаем текущие данные с сервера для сравнения try { const originalRes = await fetch(`${API}/${id}`); if (!originalRes.ok) continue; const original = await originalRes.json(); const changes = pendingChanges[id]; const updates = {}; if (original.title !== changes.title) { updates.title = changes.title; hasChanges = true; } if (original.price !== changes.price) { updates.price = changes.price; hasChanges = true; } // Сохраняем изменения количества через PATCH-запрос if (original.amount !== changes.amount) { const amountRes = await fetch(`${API}/${id}/amount`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ amount: changes.amount }) }); if (!amountRes.ok) { console.error(`Ошибка обновления количества для товара ${id}`); } } // Сохраняем изменения названия и цены if (Object.keys(updates).length > 0) { const updateRes = await fetch(`${API}/${id}`, { method: 'PATCH', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(updates) }); if (!updateRes.ok) { console.error(`Ошибка обновления товара ${id}`); } } } catch (error) { console.error(`Ошибка обновления товара ${id}:`, error); } } if (hasChanges) { loadProducts(); loadStockStats(); alert('Все изменения успешно сохранены!'); } else { alert('Нет изменений для сохранения'); } } // Загрузка начальных данных при загрузке страницы document.addEventListener('DOMContentLoaded', () => { loadProducts(); loadStockStats(); }); </script> </body> </html>