breezyducts-masterflow / mobile-app.html
007m's picture
не отображается цифры в строказ высота и ширина исправь
b98746b verified
Raw
History Blame Contribute Delete
21.8 kB
<!DOCTYPE html>
<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>