Spaces:
Running
Running
| <html lang="ru"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Конвертер Excel (XLS/XLSX) в JSON Базу Данных</title> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- FontAwesome Icons --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <!-- SheetJS для обработки XLS/XLSX файлов --> | |
| <script src="https://cdn.jsdelivr.net/npm/xlsx@0.18.5/dist/xlsx.full.min.js"></script> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| } | |
| .drag-area.active { | |
| border-color: #3b82f6; | |
| background-color: #eff6ff; | |
| } | |
| </style> | |
| </head> | |
| <body class="bg-slate-50 min-h-screen text-slate-800"> | |
| <div class="max-w-6xl mx-auto px-4 py-8"> | |
| <!-- Header --> | |
| <header class="mb-8 text-center md:text-left md:flex md:items-center md:justify-between border-b border-slate-200 pb-6"> | |
| <div> | |
| <h1 class="text-3xl font-extrabold text-slate-950 flex items-center justify-center md:justify-start gap-3"> | |
| <span class="p-2 bg-blue-600 text-white rounded-lg"><i class="fa-solid fa-database"></i></span> | |
| Конвертер Excel в JSON | |
| </h1> | |
| <p class="text-slate-500 mt-2">Преобразование вашего прайс-листа в структурированную базу данных товаров</p> | |
| </div> | |
| <div class="mt-4 md:mt-0 flex gap-3 justify-center"> | |
| <span class="inline-flex items-center gap-1.5 py-1.5 px-3 rounded-full text-xs font-medium bg-emerald-100 text-emerald-800"> | |
| <span class="w-1.5 h-1.5 inline-block rounded-full bg-emerald-500"></span> | |
| Полное соответствие шаблону | |
| </span> | |
| </div> | |
| </header> | |
| <div class="grid grid-cols-1 lg:grid-cols-3 gap-8"> | |
| <!-- Left Side: Controls & Upload --> | |
| <div class="lg:col-span-1 space-y-6"> | |
| <!-- Step 1: Upload --> | |
| <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200"> | |
| <h2 class="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2"> | |
| <span class="text-blue-600">1.</span> Загрузка файла | |
| </h2> | |
| <div id="dragArea" class="drag-area border-2 border-dashed border-slate-300 rounded-xl p-8 text-center cursor-pointer transition hover:border-blue-400 hover:bg-slate-50"> | |
| <input type="file" id="fileInput" accept=".xlsx, .xls" class="hidden"> | |
| <div class="text-4xl text-slate-400 mb-3"> | |
| <i class="fa-solid fa-file-excel"></i> | |
| </div> | |
| <p class="font-medium text-slate-700 text-sm">Выберите или перетащите Excel файл</p> | |
| <p class="text-xs text-slate-400 mt-1">Поддерживаются форматы .xlsx и .xls</p> | |
| </div> | |
| <!-- File Info (Hidden by default) --> | |
| <div id="fileInfo" class="hidden mt-4 p-3 bg-blue-50 border border-blue-100 rounded-lg flex items-center justify-between"> | |
| <div class="flex items-center gap-2 overflow-hidden"> | |
| <span class="text-blue-600 text-lg"><i class="fa-solid fa-file-lines"></i></span> | |
| <div class="truncate text-left"> | |
| <p id="fileName" class="text-xs font-semibold text-slate-700 truncate"></p> | |
| <p id="fileSize" class="text-[10px] text-slate-500"></p> | |
| </div> | |
| </div> | |
| <button id="removeFileBtn" class="text-slate-400 hover:text-red-500 transition p-1"> | |
| <i class="fa-solid fa-circle-xmark"></i> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Step 2: Custom Defaults --> | |
| <div class="bg-white p-6 rounded-2xl shadow-sm border border-slate-200"> | |
| <h2 class="text-lg font-bold text-slate-900 mb-4 flex items-center gap-2"> | |
| <span class="text-blue-600">2.</span> Настройки по умолчанию | |
| </h2> | |
| <p class="text-xs text-slate-500 mb-4">Данные значения будут автоматически подставлены в пустые поля каждого товара.</p> | |
| <div class="space-y-4"> | |
| <div> | |
| <label class="block text-xs font-semibold text-slate-600 mb-1">Штук в коробке (pieces_per_box)</label> | |
| <input type="number" id="defaultPiecesBox" value="12" class="w-full text-sm px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-blue-500"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-slate-600 mb-1">Штук в мастер-коробке (pieces_per_master_box)</label> | |
| <input type="number" id="defaultPiecesMaster" value="1200" class="w-full text-sm px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-blue-500"> | |
| </div> | |
| <div> | |
| <label class="block text-xs font-semibold text-slate-600 mb-1">Минимальный заказ (min_order)</label> | |
| <input type="number" id="defaultMinOrder" value="1" class="w-full text-sm px-3 py-2 border border-slate-300 rounded-lg focus:outline-none focus:border-blue-500"> | |
| </div> | |
| <div class="flex items-center justify-between pt-2"> | |
| <label class="text-xs font-semibold text-slate-600">Доступен к покупке (is_available)</label> | |
| <label class="relative inline-flex items-center cursor-pointer"> | |
| <input type="checkbox" id="defaultAvailable" checked class="sr-only peer"> | |
| <div class="w-11 h-6 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> | |
| </label> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Convert Button --> | |
| <button id="convertBtn" disabled class="w-full py-4 bg-slate-300 text-slate-500 font-bold rounded-2xl cursor-not-allowed transition duration-200 shadow-md flex items-center justify-center gap-2"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> Конвертировать в JSON | |
| </button> | |
| </div> | |
| <!-- Right Side: Statistics & Preview --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <!-- Statistics Dashboard (Hidden until converted) --> | |
| <div id="statsSection" class="hidden grid grid-cols-2 md:grid-cols-4 gap-4"> | |
| <div class="bg-white p-4 rounded-xl border border-slate-200 text-center shadow-sm"> | |
| <p class="text-xs text-slate-500 font-semibold uppercase tracking-wider">Всего товаров</p> | |
| <p id="statTotal" class="text-2xl font-bold text-slate-900 mt-1">0</p> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl border border-slate-200 text-center shadow-sm"> | |
| <p class="text-xs text-slate-500 font-semibold uppercase tracking-wider">Категорий</p> | |
| <p id="statCategories" class="text-2xl font-bold text-blue-600 mt-1">0</p> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl border border-slate-200 text-center shadow-sm"> | |
| <p class="text-xs text-slate-500 font-semibold uppercase tracking-wider">Мин. цена</p> | |
| <p id="statMinPrice" class="text-2xl font-bold text-slate-900 mt-1">0 ₽</p> | |
| </div> | |
| <div class="bg-white p-4 rounded-xl border border-slate-200 text-center shadow-sm"> | |
| <p class="text-xs text-slate-500 font-semibold uppercase tracking-wider">Макс. цена</p> | |
| <p id="statMaxPrice" class="text-2xl font-bold text-slate-900 mt-1">0 ₽</p> | |
| </div> | |
| </div> | |
| <!-- Preview Area --> | |
| <div class="bg-slate-900 rounded-2xl shadow-lg border border-slate-800 overflow-hidden flex flex-col h-[520px]"> | |
| <div class="bg-slate-800 px-6 py-4 flex items-center justify-between border-b border-slate-700"> | |
| <div class="flex items-center gap-2 text-white"> | |
| <span class="w-3 h-3 rounded-full bg-red-500"></span> | |
| <span class="w-3 h-3 rounded-full bg-yellow-500"></span> | |
| <span class="w-3 h-3 rounded-full bg-green-500"></span> | |
| <span class="text-xs text-slate-400 font-mono ml-2">products_output.json (Предпросмотр)</span> | |
| </div> | |
| <button id="downloadBtn" disabled class="px-4 py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white font-semibold text-xs rounded-lg transition disabled:bg-slate-700 disabled:text-slate-500 flex items-center gap-1.5"> | |
| <i class="fa-solid fa-download"></i> Скачать JSON | |
| </button> | |
| </div> | |
| <div class="p-6 flex-1 overflow-auto font-mono text-xs text-emerald-400 bg-slate-950 text-left"> | |
| <pre id="jsonPreview" class="whitespace-pre-wrap">Ожидание загрузки файла... Выберите файл слева, чтобы начать.</pre> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Alert / Toast Container --> | |
| <div id="toast" class="fixed bottom-5 right-5 max-w-sm w-full bg-white border border-slate-200 rounded-xl p-4 shadow-xl translate-y-10 opacity-0 transition duration-300 pointer-events-none flex items-start gap-3"> | |
| <span id="toastIcon" class="text-lg"></span> | |
| <div> | |
| <h3 id="toastTitle" class="text-sm font-bold text-slate-900"></h3> | |
| <p id="toastMessage" class="text-xs text-slate-500 mt-1"></p> | |
| </div> | |
| </div> | |
| <script> | |
| // DOM Elements | |
| const dragArea = document.getElementById('dragArea'); | |
| const fileInput = document.getElementById('fileInput'); | |
| const fileInfo = document.getElementById('fileInfo'); | |
| const fileName = document.getElementById('fileName'); | |
| const fileSize = document.getElementById('fileSize'); | |
| const removeFileBtn = document.getElementById('removeFileBtn'); | |
| const convertBtn = document.getElementById('convertBtn'); | |
| const downloadBtn = document.getElementById('downloadBtn'); | |
| const jsonPreview = document.getElementById('jsonPreview'); | |
| const statsSection = document.getElementById('statsSection'); | |
| // Settings inputs | |
| const defaultPiecesBox = document.getElementById('defaultPiecesBox'); | |
| const defaultPiecesMaster = document.getElementById('defaultPiecesMaster'); | |
| const defaultMinOrder = document.getElementById('defaultMinOrder'); | |
| const defaultAvailable = document.getElementById('defaultAvailable'); | |
| // Stat items | |
| const statTotal = document.getElementById('statTotal'); | |
| const statCategories = document.getElementById('statCategories'); | |
| const statMinPrice = document.getElementById('statMinPrice'); | |
| const statMaxPrice = document.getElementById('statMaxPrice'); | |
| // Toast elements | |
| const toast = document.getElementById('toast'); | |
| const toastIcon = document.getElementById('toastIcon'); | |
| const toastTitle = document.getElementById('toastTitle'); | |
| const toastMessage = document.getElementById('toastMessage'); | |
| let excelRawData = null; // Будет хранить ArrayBuffer файла | |
| let parsedJsonResult = null; | |
| // Toast Helper | |
| function showToast(title, msg, type = 'success') { | |
| toastTitle.innerText = title; | |
| toastMessage.innerText = msg; | |
| if (type === 'success') { | |
| toastIcon.innerHTML = '<i class="fa-solid fa-circle-check text-emerald-500"></i>'; | |
| toast.className = "fixed bottom-5 right-5 max-w-sm w-full bg-white border border-emerald-100 rounded-xl p-4 shadow-xl opacity-100 translate-y-0 transition duration-300"; | |
| } else { | |
| toastIcon.innerHTML = '<i class="fa-solid fa-circle-exclamation text-red-500"></i>'; | |
| toast.className = "fixed bottom-5 right-5 max-w-sm w-full bg-white border border-red-100 rounded-xl p-4 shadow-xl opacity-100 translate-y-0 transition duration-300"; | |
| } | |
| setTimeout(() => { | |
| toast.classList.add('opacity-0', 'translate-y-10'); | |
| }, 4000); | |
| } | |
| // Generate RFC4122 v4 compliant UUID in JS | |
| function generateUUID() { | |
| return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => | |
| (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) | |
| ).replace(/-/g, ''); // В шаблоне UUID без дефисов | |
| } | |
| // File Selection Drag-Drop Handlers | |
| dragArea.addEventListener('dragover', (e) => { | |
| e.preventDefault(); | |
| dragArea.classList.add('active'); | |
| }); | |
| dragArea.addEventListener('dragleave', () => { | |
| dragArea.classList.remove('active'); | |
| }); | |
| dragArea.addEventListener('drop', (e) => { | |
| e.preventDefault(); | |
| dragArea.classList.remove('active'); | |
| const file = e.dataTransfer.files[0]; | |
| handleUploadedFile(file); | |
| }); | |
| dragArea.addEventListener('click', () => { | |
| fileInput.click(); | |
| }); | |
| fileInput.addEventListener('change', (e) => { | |
| const file = e.target.files[0]; | |
| handleUploadedFile(file); | |
| }); | |
| function handleUploadedFile(file) { | |
| if (!file) return; | |
| const fileExtension = file.name.split('.').pop().toLowerCase(); | |
| if (fileExtension !== 'xlsx' && fileExtension !== 'xls') { | |
| showToast("Ошибка формата", "Пожалуйста, выберите файл .xlsx или .xls", "error"); | |
| return; | |
| } | |
| fileName.innerText = file.name; | |
| fileSize.innerText = `${(file.size / 1024).toFixed(1)} KB`; | |
| dragArea.classList.add('hidden'); | |
| fileInfo.classList.remove('hidden'); | |
| const reader = new FileReader(); | |
| reader.onload = function(e) { | |
| excelRawData = e.target.result; // Чтение файла как ArrayBuffer | |
| convertBtn.disabled = false; | |
| convertBtn.className = "w-full py-4 bg-blue-600 hover:bg-blue-500 text-white font-bold rounded-2xl cursor-pointer transition duration-200 shadow-md flex items-center justify-center gap-2"; | |
| jsonPreview.innerText = "Файл успешно загружен!\n\nНажмите кнопку 'Конвертировать в JSON' слева."; | |
| showToast("Файл принят", "Вы можете запустить конвертацию."); | |
| }; | |
| reader.readAsArrayBuffer(file); | |
| } | |
| removeFileBtn.addEventListener('click', (e) => { | |
| e.stopPropagation(); | |
| resetState(); | |
| }); | |
| function resetState() { | |
| fileInput.value = ""; | |
| excelRawData = null; | |
| parsedJsonResult = null; | |
| dragArea.classList.remove('hidden'); | |
| fileInfo.classList.add('hidden'); | |
| convertBtn.disabled = true; | |
| convertBtn.className = "w-full py-4 bg-slate-300 text-slate-500 font-bold rounded-2xl cursor-not-allowed transition duration-200 shadow-md flex items-center justify-center gap-2"; | |
| downloadBtn.disabled = true; | |
| statsSection.classList.add('hidden'); | |
| jsonPreview.innerText = "Ожидание загрузки файла... Выберите файл слева, чтобы начать."; | |
| } | |
| // Core conversion process | |
| convertBtn.addEventListener('click', () => { | |
| if (!excelRawData) return; | |
| try { | |
| // Чтение данных с помощью библиотеки SheetJS | |
| const data = new Uint8Array(excelRawData); | |
| const workbook = XLSX.read(data, { type: 'array' }); | |
| // Берем первый лист из книги | |
| const firstSheetName = workbook.SheetNames[0]; | |
| const worksheet = workbook.Sheets[firstSheetName]; | |
| // Преобразуем лист в массив массивов (строк) | |
| const rows = XLSX.utils.sheet_to_json(worksheet, { header: 1, defval: "" }); | |
| if (rows.length < 2) { | |
| throw new Error("Файл не содержит данных или строки заголовка"); | |
| } | |
| // Преобразуем заголовки к нижнему регистру для гибкого поиска | |
| const headers = rows[0].map(h => h ? h.toString().trim() : ""); | |
| // Находим индексы необходимых колонок по именам | |
| const groupIdx = headers.findIndex(h => h.toLowerCase().includes('групп')); | |
| const nameIdx = headers.findIndex(h => h.toLowerCase().includes('товар') || h.toLowerCase().includes('наименование')); | |
| const priceIdx = headers.findIndex(h => h.toLowerCase().includes('цена') || h.toLowerCase().includes('price')); | |
| if (nameIdx === -1 || priceIdx === -1) { | |
| throw new Error("Не удалось найти обязательные колонки: наименование товара или цена"); | |
| } | |
| const products = []; | |
| const categories = new Set(); | |
| let minPrice = Infinity; | |
| let maxPrice = -Infinity; | |
| const defPiecesBox = parseInt(defaultPiecesBox.value) || 12; | |
| const defPiecesMaster = parseInt(defaultPiecesMaster.value) || 1200; | |
| const defMinOrder = parseInt(defaultMinOrder.value) || 1; | |
| const defAvailable = defaultAvailable.checked; | |
| for (let i = 1; i < rows.length; i++) { | |
| const row = rows[i]; | |
| // Игнорируем пустые или неполные строки | |
| if (!row || row.length === 0) continue; | |
| const name = row[nameIdx] ? row[nameIdx].toString().trim() : ""; | |
| if (!name) continue; // Пропускаем строки без названия товара | |
| const category = groupIdx !== -1 && row[groupIdx] ? row[groupIdx].toString().trim() : "Общие"; | |
| categories.add(category); | |
| // Парсинг цены | |
| let priceVal = row[priceIdx]; | |
| let price = 0.0; | |
| if (typeof priceVal === 'number') { | |
| price = priceVal; | |
| } else if (priceVal) { | |
| let priceStr = priceVal.toString().replace(/\s/g, '').replace(',', '.'); | |
| price = parseFloat(priceStr); | |
| } | |
| if (isNaN(price)) price = 0.0; | |
| if (price < minPrice) minPrice = price; | |
| if (price > maxPrice) maxPrice = price; | |
| // Формируем продукт в соответствии с требуемой структурой | |
| const product = { | |
| "product_id": generateUUID(), | |
| "name": name, | |
| "price": price, | |
| "category": category, | |
| "stock": "", | |
| "barcode": "", | |
| "description": "", | |
| "is_available": defAvailable, | |
| "pieces_per_box": defPiecesBox, | |
| "pieces_per_master_box": defPiecesMaster, | |
| "box_price": "", | |
| "min_order": defMinOrder, | |
| "has_variant_prices": false, | |
| "variants": [] | |
| }; | |
| products.push(product); | |
| } | |
| parsedJsonResult = products; | |
| // Установка метрик | |
| statTotal.innerText = products.length; | |
| statCategories.innerText = categories.size; | |
| statMinPrice.innerText = `${minPrice === Infinity ? 0 : minPrice.toLocaleString()} ₽`; | |
| statMaxPrice.innerText = `${maxPrice === -Infinity ? 0 : maxPrice.toLocaleString()} ₽`; | |
| statsSection.classList.remove('hidden'); | |
| // Отрисовка предпросмотра JSON (первые 3 элемента для визуализации) | |
| const previewItems = products.slice(0, 3); | |
| let previewStr = JSON.stringify(previewItems, null, 2); | |
| if (products.length > 3) { | |
| previewStr = previewStr.substring(0, previewStr.length - 1) + `,\n ... еще ${products.length - 3} товаров\n]`; | |
| } | |
| jsonPreview.innerText = previewStr; | |
| downloadBtn.disabled = false; | |
| showToast("Конвертация успешна!", `Обработано ${products.length} записей без ошибок.`); | |
| } catch (err) { | |
| showToast("Ошибка конвертации", err.message, "error"); | |
| jsonPreview.innerText = "Ошибка при разборе файла:\n" + err.message; | |
| } | |
| }); | |
| // Download JSON triggers direct browser saving | |
| downloadBtn.addEventListener('click', () => { | |
| if (!parsedJsonResult) return; | |
| const jsonStr = JSON.stringify(parsedJsonResult, null, 2); | |
| const blob = new Blob([jsonStr], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const link = document.createElement('a'); | |
| link.href = url; | |
| link.download = 'products_converted.json'; | |
| document.body.appendChild(link); | |
| link.click(); | |
| document.body.removeChild(link); | |
| URL.revokeObjectURL(url); | |
| showToast("Успех!", "Файл products_converted.json сохранен."); | |
| }); | |
| </script> | |
| </body> | |
| </html> |