Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Мобильное приложение BreezyDucts</title> | |
| <link rel="manifest" href="/manifest.json"> | |
| <meta name="theme-color" content="#3b82f6"> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <script src="https://unpkg.com/feather-icons"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script> | |
| <style> | |
| .duct-animation { | |
| transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275); | |
| } | |
| .selected-item { | |
| box-shadow: 0 0 0 3px #3b82f6; | |
| } | |
| .airflow-path { | |
| stroke-dasharray: 1000; | |
| stroke-dashoffset: 1000; | |
| animation: dash 3s linear forwards; | |
| } | |
| @keyframes dash { | |
| to { | |
| stroke-dashoffset: 0; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-gray-100"> | |
| <div class="min-h-screen flex flex-col"> | |
| <!-- App Header --> | |
| <header class="bg-blue-600 text-white p-4 shadow-md sticky top-0 z-10"> | |
| <div class="flex justify-between items-center"> | |
| <div class="flex items-center space-x-2"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /> | |
| </svg> | |
| <h1 class="text-xl font-bold">BreezyDucts</h1> | |
| </div> | |
| <button id="export-btn" class="p-2 bg-blue-700 rounded-lg"> | |
| <i data-feather="download"></i> | |
| </button> | |
| </div> | |
| </header> | |
| <!-- Main App Content --> | |
| <main class="flex-1 p-4"> | |
| <!-- Category Selection --> | |
| <div class="mb-6"> | |
| <h2 class="text-lg font-semibold mb-3 text-gray-800">Выберите тип компонента</h2> | |
| <div class="grid grid-cols-3 gap-2"> | |
| <button data-category="duct" class="category-btn p-3 bg-white rounded-lg shadow-sm flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-blue-500 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /> | |
| </svg> | |
| <span class="text-xs">Воздуховоды</span> | |
| </button> | |
| <button data-category="fitting" class="category-btn p-3 bg-white rounded-lg shadow-sm flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-green-500 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z" /> | |
| </svg> | |
| <span class="text-xs">Фасонные части</span> | |
| </button> | |
| <button data-category="valve" class="category-btn p-3 bg-white rounded-lg shadow-sm flex flex-col items-center"> | |
| <svg xmlns="http://www.w3.org/2000/svg" class="h-8 w-8 text-purple-500 mb-1" fill="none" viewBox="0 0 24 24" stroke="currentColor"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 15l8-8m0 0l-8-8m8 8H4" /> | |
| </svg> | |
| <span class="text-xs">Клапаны</span> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Component Visualization --> | |
| <div class="bg-white rounded-xl shadow-md p-4 mb-6"> | |
| <div class="relative h-48 w-full mb-4"> | |
| <svg id="component-svg" viewBox="0 0 300 200" class="w-full h-full"> | |
| <!-- Dynamic content will be inserted here --> | |
| </svg> | |
| </div> | |
| <h3 id="component-name" class="text-lg font-semibold text-center text-gray-800 mb-2">Выберите компонент</h3> | |
| <p id="component-desc" class="text-sm text-gray-600 text-center">Выберите из вариантов выше</p> | |
| </div> | |
| <!-- Size Selection --> | |
| <div id="size-controls" class="hidden"> | |
| <h2 class="text-lg font-semibold mb-3 text-gray-800">Выберите размеры</h2> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-sm font-medium mb-1 text-gray-700">Б Ширина (мм)</label> | |
| <div class="flex items-center gap-2"> | |
| <input type="range" id="width-slider" min="100" max="1000" value="300" class="flex-1 h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"> | |
| <input type="number" id="width-input" min="100" max="1000" value="300" class="w-16 px-2 py-1 text-sm border border-blue-300 rounded"> | |
| <span id="width-value" class="text-sm w-16 text-center">300мм</span> | |
| </div> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1 px-1"> | |
| <span>100</span> | |
| <span>1000</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1 text-gray-700">А Высота (мм)</label> | |
| <div class="flex items-center gap-2"> | |
| <input type="range" id="height-slider" min="100" max="1000" value="300" class="flex-1 h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"> | |
| <input type="number" id="height-input" min="100" max="1000" value="300" class="w-16 px-2 py-1 text-sm border border-blue-300 rounded"> | |
| <span id="height-value" class="text-sm w-16 text-center">300мм</span> | |
| </div> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1 px-1"> | |
| <span>100</span> | |
| <span>1000</span> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-1 text-gray-700">В Длина (мм)</label> | |
| <div class="flex items-center gap-2"> | |
| <input type="range" id="length-slider" min="100" max="2000" value="500" class="flex-1 h-2 bg-blue-200 rounded-lg appearance-none cursor-pointer"> | |
| <input type="number" id="length-input" min="100" max="2000" value="500" class="w-16 px-2 py-1 text-sm border border-blue-300 rounded"> | |
| <span id="length-value" class="text-sm w-16 text-center">500мм</span> | |
| </div> | |
| <div class="flex justify-between text-xs text-gray-500 mt-1 px-1"> | |
| <span>100</span> | |
| <span>2000</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- | |
| Добавить в заказ | |
| Button --> | |
| <button id="add-to-order" class="w-full py-3 bg-blue-600 text-white rounded-lg shadow-md hover:bg-blue-700 transition hidden"> | |
| Add to Order | |
| </button> | |
| </main> | |
| <!-- Order Summary --> | |
| <div class="bg-white border-t border-gray-200 p-4 shadow-inner"> | |
| <h2 class="text-lg font-semibold mb-3 text-gray-800">Сводка заказа</h2> | |
| <div id="order-items" class="space-y-2 mb-4 max-h-40 overflow-y-auto"> | |
| <!-- Order items will be added here --> | |
| <p class="text-gray-500 text-center py-4">Ваш заказ пуст</p> | |
| </div> | |
| <div class="flex justify-between items-center text-gray-800"> | |
| <span class="font-medium">Итого:</span> | |
| <span id="order-total" class="font-bold">0 ₽</span> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Export Modal --> | |
| <div id="export-modal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center hidden z-20"> | |
| <div class="bg-white rounded-lg p-6 w-full max-w-sm mx-4"> | |
| <h3 class="text-xl font-semibold mb-4">Экспорт заказа</h3> | |
| <div class="space-y-3"> | |
| <button id="export-pdf" class="w-full py-2 px-4 bg-red-600 text-white rounded flex items-center justify-center"> | |
| <i data-feather="file-text" class="mr-2"></i> PDF документ | |
| </button> | |
| <button id="export-excel" class="w-full py-2 px-4 bg-green-600 text-white rounded flex items-center justify-center"> | |
| <i data-feather="file-text" class="mr-2"></i> Excel таблица | |
| </button> | |
| <button id="export-csv" class="w-full py-2 px-4 bg-blue-600 text-white rounded flex items-center justify-center"> | |
| <i data-feather="file-text" class="mr-2"></i> CSV файл | |
| </button> | |
| <button id="export-cancel" class="w-full py-2 px-4 bg-gray-600 text-white rounded"> | |
| Отмена | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Install button --> | |
| <div id="installContainer" class="fixed bottom-4 right-4 hidden"> | |
| <button id="installBtn" class="bg-blue-600 text-white px-4 py-2 rounded-lg shadow-lg"> | |
| <i data-feather="download" class="inline mr-2"></i> Установить | |
| </button> | |
| </div> | |
| <script> | |
| // Initialize feather icons | |
| feather.replace(); | |
| // PWA installation prompt | |
| let deferredPrompt; | |
| window.addEventListener('beforeinstallprompt', (e) => { | |
| e.preventDefault(); | |
| deferredPrompt = e; | |
| document.getElementById('installContainer').classList.remove('hidden'); | |
| }); | |
| document.getElementById('installBtn').addEventListener('click', async () => { | |
| if (!deferredPrompt) return; | |
| deferredPrompt.prompt(); | |
| const { outcome } = await deferredPrompt.userChoice; | |
| if (outcome === 'accepted') { | |
| document.getElementById('installContainer').classList.add('hidden'); | |
| } | |
| deferredPrompt = null; | |
| }); | |
| // Component data | |
| const components = { | |
| duct: [ | |
| { name: "Круглый воздуховод", desc: "Спиральные или прямые круглые воздуховоды", price: 899, svg: drawRoundDuct }, | |
| { name: "Прямоугольный воздуховод", desc: "Стандартные прямоугольные воздуховоды", price: 799, svg: drawRectDuct }, | |
| { name: "Спиральный воздуховод", desc: "Премиальные спиральные воздуховоды", price: 1299, svg: drawSpiralDuct } | |
| ], | |
| fitting: [ | |
| { name: "Отвод 90°", desc: "Стандартный отвод на 90 градусов", price: 1499, svg: drawElbow }, | |
| { name: "Отвод 45°", desc: "Стандартный отвод на 45 градусов", price: 1299, svg: drawElbow45 }, | |
| { name: "Тройник", desc: "Тройник для ответвлений", price: 1899, svg: drawTee }, | |
| { name: "Переходник", desc: "Переход между размерами", price: 1699, svg: drawReducer } | |
| ], | |
| valve: [ | |
| { name: "Бабочка", desc: "Ручная заслонка", price: 2499, svg: drawButterflyValve }, | |
| { name: "Балансировочная заслонка", desc: "Заслонка для балансировки", price: 2299, svg: drawVolumeDamper }, | |
| { name: "Противопожарная заслонка", desc: "Огнестойкая заслонка", price: 3499, svg: drawFireDamper } | |
| ] | |
| }; | |
| let currentCategory = null; | |
| let currentComponent = null; | |
| let orderItems = []; | |
| let currentSize = { width: 300, height: 300, length: 500 }; | |
| // Drawing functions for SVG components | |
| function drawRoundDuct(svg) { | |
| svg.innerHTML = ` | |
| <rect x="50" y="80" width="200" height="40" rx="20" fill="#e5e7eb" class="duct-animation" /> | |
| <path class="airflow-path" fill="none" stroke="#3b82f6" stroke-width="2" d="M50,100 Q150,60 250,100" /> | |
| <circle cx="70" cy="100" r="3" fill="#3b82f6"> | |
| <animate attributeName="cx" values="70;90;70" dur="1.5s" repeatCount="indefinite" /> | |
| </circle> | |
| <circle cx="170" cy="100" r="3" fill="#3b82f6"> | |
| <animate attributeName="cx" values="170;190;170" dur="1.5s" repeatCount="indefinite" /> | |
| </circle> | |
| <text x="150" y="30" text-anchor="middle" font-size="12" fill="#3b82f6">⌀ ${currentSize.width}мм</text> | |
| <text x="150" y="170" text-anchor="middle" font-size="12" fill="#3b82f6">${currentSize.length}мм</text> | |
| `; | |
| } | |
| function drawRectDuct(svg) { | |
| svg.innerHTML = ` | |
| <rect x="50" y="70" width="200" height="60" rx="5" fill="#e5e7eb" class="duct-animation" /> | |
| <path class="airflow-path" fill="none" stroke="#3b82f6" stroke-width="2" d="M50,100 Q150,50 250,100" /> | |
| <circle cx="70" cy="100" r="3" fill="#3b82f6"> | |
| <animate attributeName="cx" values="70;90;70" dur="1.5s" repeatCount="indefinite" /> | |
| </circle> | |
| <circle cx="170" cy="100" r="3" fill="#3b82f6"> | |
| <animate attributeName="cx" values="170;190;170" dur="1.5s" repeatCount="indefinite" /> | |
| </circle> | |
| <text x="150" y="40" text-anchor="middle" font-size="12" fill="#3b82f6">${currentSize.width}x${currentSize.height}мм</text> | |
| <text x="150" y="160" text-anchor="middle" font-size="12" fill="#3b82f6">${currentSize.length}мм</text> | |
| `; | |
| } | |
| // Other drawing functions would be similar... | |
| // Event listeners | |
| document.querySelectorAll('.category-btn').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const category = this.getAttribute('data-category'); | |
| currentCategory = category; | |
| showComponents(category); | |
| }); | |
| }); | |
| document.getElementById('width-slider').addEventListener('input', function() { | |
| currentSize.width = this.value; | |
| document.getElementById('width-input').value = this.value; | |
| document.getElementById('width-value').textContent = `${this.value}мм`; | |
| if (currentComponent) updateComponentView(); | |
| }); | |
| document.getElementById('height-slider').addEventListener('input', function() { | |
| currentSize.height = this.value; | |
| document.getElementById('height-input').value = this.value; | |
| document.getElementById('height-value').textContent = `${this.value}мм`; | |
| if (currentComponent) updateComponentView(); | |
| }); | |
| document.getElementById('length-slider').addEventListener('input', function() { | |
| currentSize.length = this.value; | |
| document.getElementById('length-input').value = this.value; | |
| document.getElementById('length-value').textContent = `${this.value}мм`; | |
| if (currentComponent) updateComponentView(); | |
| }); | |
| document.getElementById('add-to-order').addEventListener('click', function() { | |
| if (!currentComponent) return; | |
| const item = { | |
| category: currentCategory, | |
| component: currentComponent, | |
| size: {...currentSize}, | |
| price: calculatePrice() | |
| }; | |
| orderItems.push(item); | |
| updateOrderSummary(); | |
| }); | |
| document.getElementById('export-btn').addEventListener('click', function() { | |
| if (orderItems.length === 0) { | |
| alert('Your order is empty'); | |
| return; | |
| } | |
| document.getElementById('export-modal').classList.remove('hidden'); | |
| }); | |
| document.getElementById('export-cancel').addEventListener('click', function() { | |
| document.getElementById('export-modal').classList.add('hidden'); | |
| }); | |
| document.getElementById('export-pdf').addEventListener('click', function() { | |
| exportOrder('pdf'); | |
| }); | |
| document.getElementById('export-excel').addEventListener('click', function() { | |
| exportOrder('excel'); | |
| }); | |
| document.getElementById('export-csv').addEventListener('click', function() { | |
| exportOrder('csv'); | |
| }); | |
| // Helper functions | |
| function showComponents(category) { | |
| // Show component selection UI | |
| document.getElementById('size-controls').classList.add('hidden'); | |
| document.getElementById('add-to-order').classList.add('hidden'); | |
| const svg = document.getElementById('component-svg'); | |
| svg.innerHTML = ''; | |
| document.getElementById('component-name').textContent = `Выберите ${category === 'duct' ? 'воздуховод' : category === 'fitting' ? 'фасонную часть' : 'клапан'}`; | |
| document.getElementById('component-desc').textContent = 'Нажмите для выбора'; | |
| // Show component options (would be implemented with a proper UI) | |
| } | |
| function updateComponentView() { | |
| if (!currentComponent) return; | |
| const svg = document.getElementById('component-svg'); | |
| currentComponent.svg(svg); | |
| // Trigger animation | |
| const path = svg.querySelector('.airflow-path'); | |
| if (path) { | |
| path.style.animation = 'none'; | |
| void path.offsetWidth; // Trigger reflow | |
| path.style.animation = 'dash 3s linear forwards'; | |
| } | |
| } | |
| function calculatePrice() { | |
| if (!currentComponent) return 0; | |
| // Simple pricing calculation based on size | |
| const basePrice = currentComponent.price; | |
| const sizeFactor = (currentSize.width * currentSize.height * currentSize.length) / 1000000; | |
| return (basePrice * sizeFactor).toFixed(2); | |
| } | |
| function updateOrderSummary() { | |
| const orderContainer = document.getElementById('order-items'); | |
| orderContainer.innerHTML = ''; | |
| if (orderItems.length === 0) { | |
| orderContainer.innerHTML = '<p class="text-gray-500 text-center py-4">Your order is empty</p>'; | |
| document.getElementById('order-total').textContent = '$0.00'; | |
| return; | |
| } | |
| let total = 0; | |
| orderItems.forEach((item, index) => { | |
| const itemTotal = parseFloat(item.price); | |
| total += itemTotal; | |
| const itemEl = document.createElement('div'); | |
| itemEl.className = 'flex justify-between items-center p-2 bg-gray-50 rounded'; | |
| itemEl.innerHTML = ` | |
| <div> | |
| <p class="text-sm font-medium">${item.component.name}</p> | |
| <p class="text-xs text-gray-500">${item.size.width}x${item.size.height}x${item.size.length}мм</p> | |
| </div> | |
| <div class="flex items-center space-x-2"> | |
| <span class="text-sm font-medium">${itemTotal.toFixed(2)} ₽</span> | |
| <button data-index="${index}" class="delete-item p-1 text-red-500"> | |
| <i data-feather="trash-2" class="w-4 h-4"></i> | |
| </button> | |
| </div> | |
| `; | |
| orderContainer.appendChild(itemEl); | |
| }); | |
| document.getElementById('order-total').textContent = `$${total.toFixed(2)}`; | |
| // Add event listeners to delete buttons | |
| document.querySelectorAll('.delete-item').forEach(btn => { | |
| btn.addEventListener('click', function() { | |
| const index = parseInt(this.getAttribute('data-index')); | |
| orderItems.splice(index, 1); | |
| updateOrderSummary(); | |
| }); | |
| }); | |
| feather.replace(); | |
| } | |
| function exportOrder(format) { | |
| // In a real app, this would generate and download the file | |
| alert(`Экспорт заказа в формате ${format.toUpperCase()}...`); | |
| document.getElementById('export-modal').classList.add('hidden'); | |
| // For demo purposes, we'll just log the order | |
| console.log(`Exporting order (${format}):`, orderItems); | |
| } | |
| // Service Worker registration | |
| if ('serviceWorker' in navigator) { | |
| navigator.serviceWorker.register('/sw.js') | |
| .then(registration => { | |
| console.log('ServiceWorker registered'); | |
| }) | |
| .catch(err => { | |
| console.log('ServiceWorker registration failed:', err); | |
| }); | |
| } | |
| </script> | |
| </body> | |
| </html> |