- Объединены формы создания модпака и загрузки файлов в одну - Slug генерируется автоматически из названия с возможностью редактирования - Поддержка загрузки файлов сразу при создании/обновлении модпака - Автоматическое распаковывание ZIP архивов - Сохранение файлов в CAS с дедупликацией по SHA-1 - Улучшен UI с прогресс баром и валидацией форм
264 lines
9.3 KiB
HTML
264 lines
9.3 KiB
HTML
{{define "content"}}
|
|
<div class="container">
|
|
<h1>Админ-панель: Управление модпаками</h1>
|
|
|
|
<!-- Alerts -->
|
|
<div id="alert-error" class="alert alert-error"></div>
|
|
<div id="alert-success" class="alert alert-success"></div>
|
|
|
|
<!-- Modpacks List -->
|
|
<div class="card">
|
|
<h2>Список модпаков</h2>
|
|
<div id="modpacks-table">
|
|
<table>
|
|
<thead>
|
|
<tr>
|
|
<th>Slug</th>
|
|
<th>Название</th>
|
|
<th>Версия Minecraft</th>
|
|
<th>Версия Java</th>
|
|
<th>IP сервера</th>
|
|
<th>Активен</th>
|
|
<th>Действия</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody id="modpacks-tbody">
|
|
<!-- Will be filled by JavaScript -->
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unified Create & Upload Form -->
|
|
<div class="card">
|
|
<h2>Создание и загрузка модпака</h2>
|
|
<form id="modpack-form" enctype="multipart/form-data">
|
|
<div class="grid-2">
|
|
<div>
|
|
<label for="name">Название модпака</label>
|
|
<input type="text" id="name" name="name" required placeholder="Например: HiTech Server">
|
|
</div>
|
|
<div>
|
|
<label for="slug">Slug (уникальный идентификатор)</label>
|
|
<input type="text" id="slug" name="slug" required>
|
|
</div>
|
|
<div>
|
|
<label for="minecraft_version">Версия Minecraft</label>
|
|
<input type="text" id="minecraft_version" name="minecraft_version" required placeholder="Например: 1.21">
|
|
</div>
|
|
<div>
|
|
<label for="java_version">Версия Java</label>
|
|
<input type="number" id="java_version" name="java_version" min="8" required value="21">
|
|
</div>
|
|
<div>
|
|
<label for="server_ip">IP сервера</label>
|
|
<input type="text" id="server_ip" name="server_ip" placeholder="Например: play.mrixs.me:25565">
|
|
</div>
|
|
</div>
|
|
|
|
<div class="form-section">
|
|
<label for="files">Файлы модпака (ZIP архив или отдельные файлы)</label>
|
|
<input type="file" id="files" name="files" multiple>
|
|
<p>Поддерживаются архивы .zip ( будут распакованы автоматически) и отдельные файлы.</p>
|
|
</div>
|
|
|
|
<div id="upload-progress" style="display: none; margin-top: 1rem;">
|
|
<div>Прогресс загрузки: <span id="upload-percent">0%</span></div>
|
|
<progress id="upload-bar" value="0" max="100"></progress>
|
|
</div>
|
|
|
|
<button type="submit" class="btn">Создать модпак и загрузить файлы</button>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
// Helper functions
|
|
function showAlert(error, success) {
|
|
const errorAlert = document.getElementById('alert-error');
|
|
const successAlert = document.getElementById('alert-success');
|
|
if (error) {
|
|
errorAlert.textContent = error;
|
|
errorAlert.classList.add('show');
|
|
successAlert.classList.remove('show');
|
|
} else if (success) {
|
|
successAlert.textContent = success;
|
|
successAlert.classList.add('show');
|
|
errorAlert.classList.remove('show');
|
|
} else {
|
|
errorAlert.classList.remove('show');
|
|
successAlert.classList.remove('show');
|
|
}
|
|
}
|
|
|
|
function getToken() {
|
|
return localStorage.getItem('token');
|
|
}
|
|
|
|
// Generate slug from name (transliterate and clean)
|
|
function generateSlugFromName(name) {
|
|
if (!name) return '';
|
|
|
|
// Convert to lowercase and trim
|
|
let slug = name.toLowerCase().trim();
|
|
|
|
// Replace non-alphanumeric characters with hyphens
|
|
slug = slug.replace(/[^a-z0-9]+/g, '-');
|
|
|
|
// Remove leading/trailing hyphens
|
|
slug = slug.replace(/^-+|-+$/g, '');
|
|
|
|
// Limit length
|
|
slug = slug.substring(0, 32);
|
|
|
|
return slug || 'modpack';
|
|
}
|
|
|
|
// Fetch and display modpacks
|
|
async function loadModpacks() {
|
|
const token = getToken();
|
|
if (!token) {
|
|
window.location.href = '/login';
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const response = await fetch('/api/admin/modpacks', {
|
|
headers: {
|
|
'Authorization': 'Bearer ' + token
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error('Failed to fetch modpacks');
|
|
}
|
|
|
|
const modpacks = await response.json();
|
|
const tbody = document.getElementById('modpacks-tbody');
|
|
|
|
// Clear existing
|
|
tbody.innerHTML = '';
|
|
|
|
modpacks.forEach(mp => {
|
|
const tr = document.createElement('tr');
|
|
tr.innerHTML = `
|
|
<td>${mp.slug}</td>
|
|
<td>${mp.name}</td>
|
|
<td>${mp.minecraft_version}</td>
|
|
<td>${mp.java_version}</td>
|
|
<td>${mp.server_ip || '-'}</td>
|
|
<td>${mp.is_active ? 'Да' : 'Нет'}</td>
|
|
<td>
|
|
<!-- We don't have edit/delete endpoints in API yet, but we can add later -->
|
|
<button class="btn btn-sm btn-outline" onclick="loadModpackForEdit('${mp.slug}')">Редактировать</button>
|
|
</td>
|
|
`;
|
|
tbody.appendChild(tr);
|
|
});
|
|
} catch (err) {
|
|
showAlert('Ошибка загрузки модпаков: ' + err.message, null);
|
|
console.error(err);
|
|
}
|
|
}
|
|
|
|
// Handle unified form submission (create modpack + upload files)
|
|
document.getElementById('modpack-form').addEventListener('submit', async (e) => {
|
|
e.preventDefault();
|
|
const token = getToken();
|
|
if (!token) return;
|
|
|
|
const form = e.target;
|
|
const formData = new FormData(form);
|
|
|
|
// Get values
|
|
const name = formData.get('name');
|
|
let slug = formData.get('slug');
|
|
const minecraftVersion = formData.get('minecraft_version');
|
|
const javaVersionStr = formData.get('java_version');
|
|
const serverIP = formData.get('server_ip');
|
|
const files = document.getElementById('files').files;
|
|
|
|
// Auto-generate slug from name if slug field is empty or matches name-based prediction
|
|
const predictedSlug = generateSlugFromName(name);
|
|
if (!slug || slug === predictedSlug) {
|
|
slug = predictedSlug;
|
|
document.getElementById('slug').value = slug;
|
|
}
|
|
|
|
// Validate required fields
|
|
if (!slug || !name || !minecraftVersion) {
|
|
showAlert('Slug, название и версия Minecraft обязательны', null);
|
|
return;
|
|
}
|
|
|
|
// Parse java_version
|
|
const javaVersion = javaVersionStr ? parseInt(javaVersionStr) : 8;
|
|
if (isNaN(javaVersion)) {
|
|
showAlert('Неверная версия Java', null);
|
|
return;
|
|
}
|
|
|
|
// Show progress
|
|
const progressDiv = document.getElementById('upload-progress');
|
|
const percentSpan = document.getElementById('upload-percent');
|
|
const progressBar = document.getElementById('upload-bar');
|
|
progressDiv.style.display = 'block';
|
|
percentSpan.textContent = '0%';
|
|
progressBar.value = 0;
|
|
|
|
try {
|
|
// Send request to create modpack with files
|
|
const response = await fetch('/api/admin/modpacks', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': 'Bearer ' + token
|
|
},
|
|
body: formData
|
|
});
|
|
|
|
if (response.ok) {
|
|
const result = await response.json();
|
|
let message = 'Модпак успешно создан';
|
|
if result.uploaded && result.uploaded.length > 0 {
|
|
message += ` и загружено ${result.uploaded.length} файлов`;
|
|
} else if result.count && result.count > 0 {
|
|
message += ` и загружено ${result.count} файлов`;
|
|
}
|
|
showAlert(null, message);
|
|
form.reset();
|
|
document.getElementById('slug').value = ''; // Clear slug for next time
|
|
loadModpacks(); // Refresh list
|
|
} else {
|
|
const error = await response.json();
|
|
showAlert(error.errorMessage || 'Ошибка создания модпака', null);
|
|
}
|
|
} catch (err) {
|
|
showAlert('Ошибка сети: ' + err.message, null);
|
|
} finally {
|
|
progressDiv.style.display = 'none';
|
|
}
|
|
});
|
|
|
|
// Handle file input change for auto-generating slug
|
|
document.getElementById('name').addEventListener('input', function(e) {
|
|
const name = e.target.value;
|
|
const predictedSlug = generateSlugFromName(name);
|
|
|
|
// Only auto-update slug if user hasn't manually edited it
|
|
const slugInput = document.getElementById('slug');
|
|
if (!slugInput.dataset.manualEdit) {
|
|
slugInput.value = predictedSlug;
|
|
}
|
|
});
|
|
|
|
// Mark slug as manually edited when user changes it
|
|
document.getElementById('slug').addEventListener('input', function(e) {
|
|
e.target.dataset.manualEdit = 'true';
|
|
});
|
|
|
|
// Initialize
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
loadModpacks();
|
|
});
|
|
</script>
|
|
{{end}} |