1098 lines
41 KiB
HTML
1098 lines
41 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
|
||
<head>
|
||
<title>Парсер mariotexno</title>
|
||
<style>
|
||
:root {
|
||
/* Catppuccin Mocha */
|
||
--rosewater: #f5e0dc;
|
||
--flamingo: #f2cdcd;
|
||
--pink: #f5c2e7;
|
||
--mauve: #cba6f7;
|
||
--red: #f38ba8;
|
||
--maroon: #eba0ac;
|
||
--peach: #fab387;
|
||
--yellow: #f9e2af;
|
||
--green: #a6e3a1;
|
||
--teal: #94e2d5;
|
||
--sky: #89dceb;
|
||
--sapphire: #74c7ec;
|
||
--blue: #89b4fa;
|
||
--lavender: #b4befe;
|
||
--text: #cdd6f4;
|
||
--subtext1: #bac2de;
|
||
--subtext0: #a6adc8;
|
||
--overlay2: #9399b2;
|
||
--overlay1: #7f849c;
|
||
--overlay0: #6c7086;
|
||
--surface2: #585b70;
|
||
--surface1: #45475a;
|
||
--surface0: #313244;
|
||
--base: #1e1e2e;
|
||
--mantle: #181825;
|
||
--crust: #11111b;
|
||
}
|
||
|
||
* {
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
body {
|
||
font-family: 'Inter', sans-serif;
|
||
max-width: 800px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
color: var(--text);
|
||
background: var(--base);
|
||
}
|
||
|
||
h1,
|
||
h2,
|
||
h3 {
|
||
color: var(--text);
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.form-group {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
input[type="text"],
|
||
input[type="number"] {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
margin-top: 0.5rem;
|
||
border: 1px solid var(--surface1);
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
background: var(--surface0);
|
||
color: var(--text);
|
||
transition: border-color 0.2s;
|
||
}
|
||
|
||
input[type="text"]:focus,
|
||
input[type="number"]:focus {
|
||
outline: none;
|
||
border-color: var(--blue);
|
||
}
|
||
|
||
/* Убираем стрелки для input number */
|
||
input[type="number"]::-webkit-inner-spin-button,
|
||
input[type="number"]::-webkit-outer-spin-button {
|
||
-webkit-appearance: none;
|
||
margin: 0;
|
||
}
|
||
|
||
input[type="number"] {
|
||
-moz-appearance: textfield;
|
||
}
|
||
|
||
button {
|
||
padding: 0.75rem 1.5rem;
|
||
background-color: var(--blue);
|
||
color: var(--base);
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 1rem;
|
||
font-weight: 500;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
button:hover {
|
||
background-color: var(--lavender);
|
||
}
|
||
|
||
button:disabled {
|
||
background-color: var(--overlay0);
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.status {
|
||
margin: 1.5rem 0;
|
||
padding: 1rem;
|
||
border: 1px solid var(--surface1);
|
||
border-radius: 8px;
|
||
background-color: var(--surface0);
|
||
}
|
||
|
||
.error {
|
||
color: var(--red);
|
||
}
|
||
|
||
.files {
|
||
margin-top: 2rem;
|
||
}
|
||
|
||
.files ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.files li {
|
||
margin: 0.75rem 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
padding: 1rem;
|
||
border: 1px solid var(--surface1);
|
||
border-radius: 8px;
|
||
transition: all 0.2s;
|
||
background: var(--surface0);
|
||
}
|
||
|
||
.files li:hover {
|
||
background-color: var(--surface1);
|
||
}
|
||
|
||
.file-info {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: flex-end;
|
||
gap: 1.5rem;
|
||
width: 100%;
|
||
}
|
||
|
||
.download-link {
|
||
color: var(--text);
|
||
text-decoration: none;
|
||
position: relative;
|
||
padding-left: 2rem;
|
||
margin-right: auto;
|
||
font-weight: 500;
|
||
transition: color 0.2s;
|
||
}
|
||
|
||
.download-link:hover {
|
||
color: var(--blue);
|
||
}
|
||
|
||
.download-link::before {
|
||
content: "📥";
|
||
position: absolute;
|
||
left: 0;
|
||
top: 50%;
|
||
transform: translateY(-50%);
|
||
}
|
||
|
||
.file-date,
|
||
.file-size {
|
||
color: var(--subtext0);
|
||
font-size: 0.9rem;
|
||
}
|
||
|
||
.delete-btn {
|
||
background: none;
|
||
border: none;
|
||
color: var(--red);
|
||
cursor: pointer;
|
||
padding: 0.5rem;
|
||
font-size: 1.2rem;
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
color: var(--maroon);
|
||
}
|
||
|
||
/* Стилі для вкладок */
|
||
.tabs {
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.tab-buttons {
|
||
display: flex;
|
||
gap: 10px;
|
||
margin-bottom: 20px;
|
||
align-items: center;
|
||
justify-content: flex-start;
|
||
}
|
||
|
||
.tab-button {
|
||
height: 44px;
|
||
padding: 0 24px;
|
||
background-color: var(--surface0);
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 14px;
|
||
color: var(--subtext0);
|
||
transition: all 0.2s;
|
||
}
|
||
|
||
.tab-button:hover {
|
||
background-color: var(--surface1);
|
||
color: var(--text);
|
||
}
|
||
|
||
.tab-button.active {
|
||
background-color: var(--blue);
|
||
color: var(--base);
|
||
}
|
||
|
||
.tab-content {
|
||
display: none;
|
||
background: var(--surface0);
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.tab-content.active {
|
||
display: block;
|
||
}
|
||
|
||
.file-select {
|
||
width: 100%;
|
||
padding: 0.75rem;
|
||
margin-top: 0.5rem;
|
||
border: 1px solid var(--surface1);
|
||
border-radius: 8px;
|
||
font-size: 1rem;
|
||
background: var(--surface0);
|
||
color: var(--text);
|
||
transition: border-color 0.2s;
|
||
-webkit-appearance: none;
|
||
-moz-appearance: none;
|
||
appearance: none;
|
||
background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23cdd6f4' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6 9 12 15 18 9'%3e%3c/polyline%3e%3c/svg%3e");
|
||
background-repeat: no-repeat;
|
||
background-position: right 0.75rem center;
|
||
background-size: 1em;
|
||
}
|
||
|
||
.file-select:focus {
|
||
outline: none;
|
||
border-color: var(--blue);
|
||
}
|
||
|
||
.items-limit-group {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 0.5rem;
|
||
margin-left: auto;
|
||
height: 44px;
|
||
}
|
||
|
||
.items-limit-group label {
|
||
color: var(--subtext0);
|
||
font-size: 14px;
|
||
}
|
||
|
||
#items-limit {
|
||
margin: 0;
|
||
text-align: center;
|
||
max-width: 4rem;
|
||
}
|
||
|
||
.items-limit-group input[type="number"] {
|
||
height: 44px;
|
||
width: 4rem;
|
||
padding: 0 0.75rem;
|
||
flex-shrink: 0;
|
||
text-align: center;
|
||
}
|
||
|
||
.categories-section {
|
||
margin-bottom: 2rem;
|
||
background: var(--surface0);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.category-form {
|
||
display: flex;
|
||
gap: 1rem;
|
||
align-items: flex-end;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.category-form .form-group {
|
||
margin-bottom: 0;
|
||
}
|
||
|
||
.category-form button {
|
||
flex: 0 0 auto;
|
||
}
|
||
|
||
.secondary-button {
|
||
background-color: var(--surface1);
|
||
color: var(--text);
|
||
}
|
||
|
||
.secondary-button:hover {
|
||
background-color: var(--surface2);
|
||
}
|
||
|
||
.categories-list ul {
|
||
list-style: none;
|
||
padding: 0;
|
||
}
|
||
|
||
.categories-list li {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
padding: 0.5rem;
|
||
margin: 0.5rem 0;
|
||
background: var(--surface1);
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.yml-generation-section {
|
||
margin-bottom: 2rem;
|
||
background: var(--surface0);
|
||
border-radius: 8px;
|
||
}
|
||
|
||
.loading {
|
||
position: relative;
|
||
pointer-events: none;
|
||
opacity: 0.7;
|
||
}
|
||
|
||
.loading::after {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
width: 20px;
|
||
height: 20px;
|
||
margin: -10px 0 0 -10px;
|
||
border: 2px solid var(--blue);
|
||
border-top-color: transparent;
|
||
border-radius: 50%;
|
||
animation: spin 1s linear infinite;
|
||
}
|
||
|
||
@keyframes spin {
|
||
to { transform: rotate(360deg); }
|
||
}
|
||
|
||
.error-message {
|
||
position: fixed;
|
||
top: 20px;
|
||
right: 20px;
|
||
padding: 1rem;
|
||
background: var(--red);
|
||
color: var(--base);
|
||
border-radius: 8px;
|
||
animation: slideIn 0.3s ease-out;
|
||
}
|
||
|
||
@keyframes slideIn {
|
||
from { transform: translateX(100%); }
|
||
to { transform: translateX(0); }
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<h1>Парсер euro.com.pl</h1>
|
||
|
||
<div class="tabs">
|
||
<div class="tab-buttons">
|
||
<button class="tab-button active" onclick="openTab('parser')">Парсер</button>
|
||
<button class="tab-button" onclick="openTab('processor')">Переклад</button>
|
||
<button class="tab-button" onclick="openTab('generator')">YML Фід</button>
|
||
<button class="tab-button" onclick="openTab('categories')">Категорії</button>
|
||
<div class="items-limit-group">
|
||
<label for="items-limit">Ліміт:</label>
|
||
<input type="number" id="items-limit" value="{{ app_settings.items_limit }}" min="-1"
|
||
onchange="updateItemsLimit(this.value)" title="Встановіть -1 для зняття обмежень">
|
||
</div>
|
||
<button onclick="refreshOldestCategory()">🔄</button>
|
||
</div>
|
||
|
||
|
||
|
||
<!-- Вкладка парсера -->
|
||
<div id="parser" class="tab-content active">
|
||
<div class="form-group">
|
||
<label for="url">URL категорії:</label>
|
||
<input type="text" id="url" placeholder="https://www.euro.com.pl/odkurzacze-automatyczne.bhtml">
|
||
</div>
|
||
|
||
<button onclick="startParsing()" id="parseButton">Почати парсинг</button>
|
||
|
||
<div class="status" id="status">
|
||
{% if status.is_running %}
|
||
<p>Парсинг запущено для категорії: {{ status.current_category }}</p>
|
||
{% else %}
|
||
<p>Парсер не активний</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="files">
|
||
<h3>Збережені результати:</h3>
|
||
<ul>
|
||
{% for file in parsed_files %}
|
||
<li>
|
||
<div class="file-info">
|
||
<a href="{{ url_for('download_file', filename=file.name, directory='output') }}"
|
||
class="download-link" download>
|
||
{{ file.name }}
|
||
</a>
|
||
<span class="file-date">{{ file.modified }}</span>
|
||
<span class="file-size">{{ file.size }}</span>
|
||
</div>
|
||
<button class="delete-btn" onclick="deleteFile('{{ file.name }}', 'output')"
|
||
title="Видалити файл">
|
||
🗑️
|
||
</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Вкладка оробки даних -->
|
||
<div id="processor" class="tab-content">
|
||
<div class="form-group">
|
||
<label for="file-select">Виберіть файл для обробки:</label>
|
||
<select id="file-select" class="file-select">
|
||
<option value="">Виберіть файл...</option>
|
||
{% for file in parsed_files %}
|
||
<option value="{{ file.name }}">{{ file.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<button onclick="startTranslation()" id="translateButton">Почати переклад</button>
|
||
|
||
<div class="status" id="translation-status">
|
||
{% if translation_status.is_running %}
|
||
<p>Переклад в процесі...<br>
|
||
Оброблено: {{ translation_status.processed_items }} з {{ translation_status.total_items }}</p>
|
||
{% else %}
|
||
<p>Переклад не активний</p>
|
||
{% endif %}
|
||
</div>
|
||
|
||
<div class="files">
|
||
<h3>Перекладені файли:</h3>
|
||
<ul>
|
||
{% for file in translated_files %}
|
||
<li>
|
||
<div class="file-info">
|
||
<a href="{{ url_for('download_file', filename=file.name, directory='translated') }}"
|
||
class="download-link" download>
|
||
{{ file.name }}
|
||
</a>
|
||
<span class="file-date">{{ file.modified }}</span>
|
||
<span class="file-size">{{ file.size }}</span>
|
||
</div>
|
||
<button class="delete-btn" onclick="deleteFile('{{ file.name }}', 'translated')"
|
||
title="Видалити файл">
|
||
🗑️
|
||
</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Вкладка генератора YML -->
|
||
<div id="generator" class="tab-content">
|
||
<div class="yml-generation-section">
|
||
<div class="form-group">
|
||
<label for="yml-category-select">Категорія:</label>
|
||
<select id="yml-category-select" class="file-select">
|
||
<option value="">Виберіть категорію...</option>
|
||
{% for category in categories %}
|
||
<option value="{{ category.id }}">{{ category.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label for="yml-file-select">Файл для генерації:</label>
|
||
<select id="yml-file-select" class="file-select">
|
||
<option value="">Виберіть файл...</option>
|
||
{% for file in translated_files %}
|
||
<option value="{{ file.name }}">{{ file.name }}</option>
|
||
{% endfor %}
|
||
</select>
|
||
</div>
|
||
|
||
<button onclick="generateYML()" id="generateButton" class="action-button">Згенерувати YML</button>
|
||
</div>
|
||
|
||
<div class="status" id="yml-status">
|
||
<p>Генератор не активний</p>
|
||
</div>
|
||
|
||
<div class="files">
|
||
<h3>Згенеровані YML файли:</h3>
|
||
<ul>
|
||
{% for file in yml_files %}
|
||
<li>
|
||
<div class="file-info">
|
||
<a href="{{ url_for('download_file', filename=file.name|urlencode, directory='yml') }}"
|
||
class="download-link" download>
|
||
{{ file.name }}
|
||
</a>
|
||
<span class="file-date">{{ file.modified }}</span>
|
||
<span class="file-size">{{ file.size }}</span>
|
||
</div>
|
||
<button class="delete-btn" onclick="deleteFile('{{ file.name }}', 'yml')"
|
||
title="Видалити файл">
|
||
🗑️
|
||
</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<!-- Вкладка управления категориями -->
|
||
<div id="categories" class="tab-content">
|
||
<div class="categories-section">
|
||
<div class="category-form">
|
||
<div class="form-group">
|
||
<label for="category-id">ID категорії:</label>
|
||
<input type="text" id="category-id" placeholder="1">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="category-name">Назва категорії:</label>
|
||
<input type="text" id="category-name" placeholder="Роботи-пилососи">
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="portal-id">ID категорії Prom.ua:</label>
|
||
<input type="text" id="portal-id" placeholder="12345">
|
||
</div>
|
||
<button onclick="addCategory()" class="secondary-button">Додати категорію</button>
|
||
</div>
|
||
|
||
|
||
<div class="categories-list">
|
||
<h4>Збережені категорії:</h4>
|
||
<ul id="categories-list">
|
||
{% for category in categories %}
|
||
<li>
|
||
<span>{{ category.id }} - {{ category.name }} {% if category.portal_id %} (portal_id: {{ category.portal_id }}){% endif %}</span>
|
||
<button class="delete-btn" onclick="deleteCategory('{{ category.id }}')" title="Видалити категорію">
|
||
🗑️
|
||
</button>
|
||
</li>
|
||
{% endfor %}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script src="https://code.jquery.com/jquery-3.7.1.min.js" integrity="sha256-/JqT3SQfawRcv/BIHPThkBvs0OEvtFFmqPF/lYI/Cxo=" crossorigin="anonymous"></script>
|
||
|
||
<script>
|
||
let statusCheckInterval;
|
||
|
||
function startStatusCheck() {
|
||
// Очищаем предыдущий интервал, если он существует
|
||
if (statusCheckInterval) {
|
||
clearInterval(statusCheckInterval);
|
||
}
|
||
// Проверяем статус каждые 500мс
|
||
statusCheckInterval = setInterval(checkStatus, 500);
|
||
// Сразу делаем первую проверку
|
||
checkStatus();
|
||
}
|
||
|
||
function checkStatus() {
|
||
fetch('/status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const status = document.getElementById('status');
|
||
const button = document.getElementById('parseButton');
|
||
|
||
if (data.is_running) {
|
||
let statusHtml = `
|
||
<p>
|
||
Парсинг запущено для категорії: ${data.current_category}<br>
|
||
`;
|
||
|
||
if (data.total_products > 0) {
|
||
statusHtml += `
|
||
<strong>Товар ${data.current_product} із ${data.total_products}</strong><br>
|
||
Прогрес: ${data.progress}%
|
||
`;
|
||
} else {
|
||
statusHtml += `Отримання інформації про кількість товарів...`;
|
||
}
|
||
|
||
statusHtml += `</p>`;
|
||
status.innerHTML = statusHtml;
|
||
setTimeout(checkStatus, 500);
|
||
} else {
|
||
if (data.error) {
|
||
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
||
} else if (data.total_products > 0) {
|
||
status.innerHTML = `
|
||
<p>
|
||
Парсинг завершено<br>
|
||
<strong>Всього оброблено товарів: ${data.total_products}</strong>
|
||
</p>
|
||
`;
|
||
updateFilesList('parsed', 'parser', 'file-select');
|
||
}
|
||
button.disabled = false;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Status check error:', error);
|
||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||
button.disabled = false;
|
||
});
|
||
}
|
||
|
||
function stopStatusCheck() {
|
||
if (statusCheckInterval) {
|
||
clearInterval(statusCheckInterval);
|
||
statusCheckInterval = null;
|
||
}
|
||
}
|
||
|
||
function deleteFile(filename, type) {
|
||
if (confirm('Ви впевнені, що хочете видалити цей файл?')) {
|
||
fetch(`/delete/${filename}`, {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
alert(data.error);
|
||
} else {
|
||
// Удаляем файл из списка
|
||
const fileItem = document.querySelector(`a[href*="${filename}"]`).closest('li');
|
||
fileItem.remove();
|
||
|
||
// Обновляем соответствующий select в зависимости от типа файла
|
||
if (type === 'parsed') {
|
||
const fileSelect = document.getElementById('file-select');
|
||
const optionToRemove = Array.from(fileSelect.options).find(opt => opt.value === filename);
|
||
if (optionToRemove) {
|
||
optionToRemove.remove();
|
||
}
|
||
} else if (type === 'translated') {
|
||
const ymlFileSelect = document.getElementById('yml-file-select');
|
||
const optionToRemove = Array.from(ymlFileSelect.options).find(opt => opt.value === filename);
|
||
if (optionToRemove) {
|
||
optionToRemove.remove();
|
||
}
|
||
}
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
alert('Помилка при видаленні файлу');
|
||
});
|
||
}
|
||
}
|
||
|
||
function openTab(tabName) {
|
||
// Приховуємо всі вкладки
|
||
const tabContents = document.getElementsByClassName('tab-content');
|
||
for (let content of tabContents) {
|
||
content.classList.remove('active');
|
||
}
|
||
|
||
// Деактивуємо всі кнопки
|
||
const tabButtons = document.getElementsByClassName('tab-button');
|
||
for (let button of tabButtons) {
|
||
button.classList.remove('active');
|
||
}
|
||
|
||
// Показуємо вибрану вкладку
|
||
document.getElementById(tabName).classList.add('active');
|
||
|
||
// Активуємо потрібну кнопку
|
||
event.currentTarget.classList.add('active');
|
||
}
|
||
|
||
function startTranslation() {
|
||
const fileSelect = document.getElementById('file-select');
|
||
const button = document.getElementById('translateButton');
|
||
const status = document.getElementById('translation-status');
|
||
|
||
if (!fileSelect.value) {
|
||
status.innerHTML = '<p class="error">Будь ласка, виберіть файл для обробки</p>';
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
status.innerHTML = '<p>Починаємо переклад...</p>';
|
||
|
||
fetch('/translate', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `filename=${encodeURIComponent(fileSelect.value)}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
status.innerHTML = `<p class="error">${data.error}</p>`;
|
||
button.disabled = false;
|
||
} else {
|
||
checkTranslationStatus();
|
||
}
|
||
});
|
||
}
|
||
|
||
function checkTranslationStatus() {
|
||
fetch('/translation-status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const status = document.getElementById('translation-status');
|
||
const button = document.getElementById('translateButton');
|
||
|
||
if (data.is_running) {
|
||
if (data.total_items > 0) {
|
||
const percent = Math.round((data.processed_items / data.total_items) * 100);
|
||
status.innerHTML = `
|
||
<p>
|
||
Переклад в процесі...<br>
|
||
Оброблено: ${data.processed_items} з ${data.total_items}<br>
|
||
Прогрес: ${percent}%
|
||
</p>
|
||
`;
|
||
} else {
|
||
status.innerHTML = '<p>Підготовка до перекладу...</p>';
|
||
}
|
||
setTimeout(checkTranslationStatus, 1000);
|
||
} else {
|
||
if (data.error) {
|
||
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
||
} else if (data.total_items > 0) {
|
||
status.innerHTML = `
|
||
<p>
|
||
Переклад завершено<br>
|
||
Всього оброблено товарів: ${data.total_items}
|
||
</p>
|
||
`;
|
||
updateFilesList('translated', 'processor', 'yml-file-select');
|
||
}
|
||
button.disabled = false;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Translation status check error:', error);
|
||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||
document.getElementById('translateButton').disabled = false;
|
||
});
|
||
}
|
||
|
||
function updateItemsLimit(value) {
|
||
fetch('/update-settings', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
items_limit: parseInt(value)
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
alert(data.error);
|
||
}
|
||
});
|
||
}
|
||
|
||
function addCategory() {
|
||
const categoryId = document.getElementById('category-id').value;
|
||
const categoryName = document.getElementById('category-name').value;
|
||
const portalId = document.getElementById('portal-id').value;
|
||
|
||
if (!categoryId || !categoryName) {
|
||
alert('Будь ласка, заповніть всі поля');
|
||
return;
|
||
}
|
||
|
||
fetch('/add-category', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
id: categoryId,
|
||
name: categoryName,
|
||
portal_id: portalId
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
alert(data.error);
|
||
} else {
|
||
// Очищаем поля ввода
|
||
document.getElementById('category-id').value = '';
|
||
document.getElementById('category-name').value = '';
|
||
document.getElementById('portal-id').value = '';
|
||
|
||
// Добавляем новую категорию в список
|
||
const categoriesList = document.getElementById('categories-list');
|
||
const li = document.createElement('li');
|
||
li.innerHTML = `
|
||
<span>${categoryId} - ${categoryName}</span>
|
||
<button class="delete-btn" onclick="deleteCategory('${categoryId}')" title="Видалити категорію">
|
||
️
|
||
</button>
|
||
`;
|
||
categoriesList.appendChild(li);
|
||
|
||
// Обновляем select в генераторе YML
|
||
const ymlSelect = document.getElementById('yml-category-select');
|
||
const option = document.createElement('option');
|
||
option.value = categoryId;
|
||
option.text = categoryName;
|
||
ymlSelect.appendChild(option);
|
||
}
|
||
});
|
||
}
|
||
|
||
function deleteCategory(categoryId) {
|
||
if (confirm('Ви впевнені, що хочете видалити цю категорію?')) {
|
||
fetch('/delete-category', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
id: categoryId
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
alert(data.error);
|
||
} else {
|
||
// Удаляем категорию из списка
|
||
const categoryItems = document.querySelectorAll('#categories-list li');
|
||
categoryItems.forEach(item => {
|
||
if (item.querySelector('span').textContent.startsWith(categoryId + ' -')) {
|
||
item.remove();
|
||
}
|
||
});
|
||
|
||
// Удаляем опцию из select в генераторе YML
|
||
const ymlSelect = document.getElementById('yml-category-select');
|
||
const option = Array.from(ymlSelect.options).find(opt => opt.value === categoryId);
|
||
if (option) option.remove();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
function generateYML() {
|
||
console.log('generateYML called'); // Отладка
|
||
const categoryId = document.getElementById('yml-category-select').value;
|
||
const fileSelect = document.getElementById('yml-file-select');
|
||
console.log('Selected category:', categoryId); // Отладка
|
||
console.log('Selected file:', fileSelect.value); // Отладка
|
||
const button = document.getElementById('generateButton');
|
||
const status = document.getElementById('yml-status');
|
||
|
||
if (!categoryId || !fileSelect.value) {
|
||
status.innerHTML = '<p class="error">Будь ласка, виберіть категорію та файл</p>';
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
status.innerHTML = '<p>Генерація YML...</p>';
|
||
|
||
fetch('/generate-yml', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
},
|
||
body: JSON.stringify({
|
||
filename: fileSelect.value,
|
||
category_id: categoryId
|
||
})
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
status.innerHTML = `<p class="error">${data.error}</p>`;
|
||
} else {
|
||
status.innerHTML = '<p>YML файл успішно згенеровано</p>';
|
||
updateFilesList('yml', 'generator', null);
|
||
}
|
||
button.disabled = false;
|
||
})
|
||
.catch(error => {
|
||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||
button.disabled = false;
|
||
});
|
||
}
|
||
|
||
// Добавляем вспомогательную функцию для поиска по тексту
|
||
jQuery.expr[':'].contains = function(a, i, m) {
|
||
return jQuery(a).text().toUpperCase()
|
||
.indexOf(m[3].toUpperCase()) >= 0;
|
||
};
|
||
|
||
function updateFilesList(fileType, containerId, selectId = null) {
|
||
fetch(`/get-files/${fileType}`)
|
||
.then(response => response.json())
|
||
.then(files => {
|
||
// Обновляем список файлов
|
||
const filesList = document.querySelector(`#${containerId} .files ul`);
|
||
filesList.innerHTML = files.map(file => `
|
||
<li>
|
||
<div class="file-info">
|
||
<a href="/download/${file.name}" class="download-link" download>
|
||
${file.name}
|
||
</a>
|
||
<span class="file-date">${file.modified}</span>
|
||
<span class="file-size">${file.size}</span>
|
||
</div>
|
||
<button class="delete-btn" onclick="deleteFile('${file.name}', '${fileType}')" title="Видалити файл">
|
||
🗑️
|
||
</button>
|
||
</li>
|
||
`).join('');
|
||
|
||
// Обновляем select, если указан
|
||
if (selectId) {
|
||
const select = document.getElementById(selectId);
|
||
select.innerHTML = '<option value="">Виберіть файл...</option>' +
|
||
files.map(file => `
|
||
<option value="${file.name}">${file.name}</option>
|
||
`).join('');
|
||
}
|
||
});
|
||
}
|
||
|
||
function showLoader(element) {
|
||
element.classList.add('loading');
|
||
}
|
||
|
||
function hideLoader(element) {
|
||
element.classList.remove('loading');
|
||
}
|
||
|
||
function showError(message) {
|
||
const errorDiv = document.createElement('div');
|
||
errorDiv.className = 'error-message';
|
||
errorDiv.textContent = message;
|
||
document.body.appendChild(errorDiv);
|
||
setTimeout(() => errorDiv.remove(), 3000);
|
||
}
|
||
|
||
// Использование:
|
||
async function someAction() {
|
||
const element = document.getElementById('someElement');
|
||
showLoader(element);
|
||
try {
|
||
const response = await fetch('/some-endpoint');
|
||
const data = await response.json();
|
||
if (data.error) throw new Error(data.error);
|
||
// обработка успешного ответа
|
||
} catch (error) {
|
||
showError(error.message);
|
||
} finally {
|
||
hideLoader(element);
|
||
}
|
||
}
|
||
|
||
function startParsing() {
|
||
const url = document.getElementById('url').value;
|
||
const button = document.getElementById('parseButton');
|
||
const status = document.getElementById('status');
|
||
|
||
if (!url) {
|
||
status.innerHTML = '<p class="error">Будь ласка, введіть URL</p>';
|
||
return;
|
||
}
|
||
|
||
button.disabled = true;
|
||
status.innerHTML = '<p>Починаємо парсинг...</p>';
|
||
|
||
fetch('/parse', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/x-www-form-urlencoded',
|
||
},
|
||
body: `url=${encodeURIComponent(url)}`
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.error) {
|
||
status.innerHTML = `<p class="error">${data.error}</p>`;
|
||
button.disabled = false;
|
||
} else {
|
||
checkParsingStatus();
|
||
}
|
||
})
|
||
.catch(error => {
|
||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||
button.disabled = false;
|
||
});
|
||
}
|
||
|
||
function checkParsingStatus() {
|
||
fetch('/status')
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
const status = document.getElementById('status');
|
||
const button = document.getElementById('parseButton');
|
||
|
||
if (data.is_running) {
|
||
if (data.total_items > 0) {
|
||
const percent = Math.round((data.processed_items / data.total_items) * 100);
|
||
status.innerHTML = `
|
||
<p>
|
||
Парсинг в процесі...<br>
|
||
Оброблено: ${data.processed_items} з ${data.total_items}<br>
|
||
Прогрес: ${percent}%
|
||
</p>
|
||
`;
|
||
} else {
|
||
status.innerHTML = '<p>Отримання інформації про товари...</p>';
|
||
}
|
||
setTimeout(checkParsingStatus, 1000);
|
||
} else {
|
||
if (data.error) {
|
||
status.innerHTML = `<p class="error">Помилка: ${data.error}</p>`;
|
||
} else {
|
||
status.innerHTML = `
|
||
<p>
|
||
Парсинг завершено<br>
|
||
Всього оброблено товарів: ${data.total_items}
|
||
</p>
|
||
`;
|
||
updateFilesList('parsed', 'parser', 'file-select');
|
||
}
|
||
button.disabled = false;
|
||
}
|
||
})
|
||
.catch(error => {
|
||
console.error('Status check error:', error);
|
||
status.innerHTML = `<p class="error">Помилка: ${error.message}</p>`;
|
||
document.getElementById('parseButton').disabled = false;
|
||
});
|
||
}
|
||
|
||
function refreshOldestCategory() {
|
||
fetch('/auto-refresh', {
|
||
method: 'POST'
|
||
})
|
||
.then(response => response.json())
|
||
.then(data => {
|
||
if (data.success) {
|
||
alert(`Оновлено: ${data.category}`);
|
||
} else {
|
||
alert(data.error || 'Помилка під час оновлення');
|
||
}
|
||
});
|
||
}
|
||
</script>
|
||
</body>
|
||
|
||
</html>
|