converterbase / index.html
Kgshop's picture
Update index.html
1987cf0 verified
Raw
History Blame Contribute Delete
24.3 kB
<!DOCTYPE html>
<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>